Compare commits

...

5 Commits

Author SHA1 Message Date
pschneid
7ed44ac60e fix 'currently listening' bug 2025-10-17 09:09:04 +02:00
pschneid
af5b20d3e6 fix bug about sending playlist pictures 2025-10-16 15:52:07 +02:00
pschneid
c678b60b69 update frontend and add album covers 2025-10-16 13:07:44 +02:00
pschneid
9f45a9c43f upgrade frontend and add backend/api functionality 2025-10-15 16:24:16 +02:00
pschneid
0f6605a1ce update https 2025-10-14 14:08:06 +02:00
1919 changed files with 963288 additions and 7883 deletions

201
README.md
View File

@ -1,145 +1,120 @@
# Harmony — A Shared Spotify Experience for Two 💕
# 💕 Our Musical Journey
A beautiful, romantic web application that allows two Spotify users to share their musical journey together. Built with Next.js, TypeScript, and Tailwind CSS.
A beautiful, modern web application that connects two hearts through music. Built with React, TypeScript, and the Spotify Web API.
## Features
## Features
### 🎵 Shared Timeline
- View recently played songs from both users in a beautiful timeline
- See which songs you both have listened to with special heart markers 💕
- Track your musical journey together over time
### 🎵 Last Listened
- See what your partner is listening to in real-time
- Play their music directly from the app
- View recent listening history
### 🎧 Mix Generator
- AI-powered playlist creation based on both users' musical tastes
- Uses Spotify's Recommendations API to find perfect songs for both of you
- One-click playlist creation and saving to Spotify
### 🎨 Mixed Playlist
- AI-powered playlist generation
- Analyzes both users' music tastes
- Creates perfect blends of musical preferences
- Save playlists to Spotify or locally
### 📊 Live Dashboard
- Real-time display of what both users are currently listening to
- Harmony percentage calculation based on audio features (BPM, energy, valence)
- Beautiful animated progress bars and album covers
### 💖 Memory Lane
- Create beautiful musical memories together
- Timeline of shared musical experiences
- Track milestones and special moments
- Add custom memories and stories
### ⚙️ Settings
- Customize display names and profile pictures
- Theme selection (Rose, Purple, Blue)
- Secure user authentication with Spotify OAuth
## 🚀 Getting Started
## Tech Stack
- **Frontend**: Next.js 15, React 19, TypeScript, Tailwind CSS
- **Animations**: Framer Motion
- **Icons**: Lucide React
- **Backend**: Next.js API Routes
- **Database**: PostgreSQL with Prisma ORM
- **Authentication**: NextAuth.js with Spotify OAuth
- **Music API**: Spotify Web API
## Setup Instructions
### 1. Prerequisites
- Node.js 18+
- PostgreSQL database
### Prerequisites
- Node.js 18+ and npm
- Spotify Developer Account
- Two Spotify Premium accounts (for full functionality)
### 2. Database Setup
```bash
# Create PostgreSQL database
createdb spotify_app
### Setup
# Set up database user (optional, can use default)
psql -c "CREATE USER iu WITH PASSWORD 'iu';"
psql -c "GRANT ALL PRIVILEGES ON DATABASE spotify_app TO iu;"
```
1. **Clone and install dependencies:**
```bash
cd spotify
npm install
```
### 3. Environment Configuration
Copy `.env.local` and update with your credentials:
2. **Set up Spotify App:**
- Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard)
- Create a new app
- Add `http://localhost:3000/callback` to redirect URIs
- Copy your Client ID and Client Secret
```env
# Database
DATABASE_URL="postgresql://iu:iu@localhost:5432/spotify_app?schema=public"
3. **Configure environment:**
```bash
cp env.example .env.local
```
Edit `.env.local` with your Spotify credentials:
```
VITE_SPOTIFY_CLIENT_ID=your_client_id
VITE_SPOTIFY_CLIENT_SECRET=your_client_secret
VITE_REDIRECT_URI=http://localhost:3000/callback
```
# NextAuth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-secret-key-here"
4. **Start the development server:**
```bash
npm run dev
```
# Spotify OAuth
SPOTIFY_CLIENT_ID="your-spotify-client-id"
SPOTIFY_CLIENT_SECRET="your-spotify-client-secret"
5. **Open your browser:**
Navigate to `http://localhost:3000`
# Allowed Spotify Users (comma-separated Spotify user IDs)
ALLOWED_SPOTIFY_USERS="your-spotify-user-id,partner-spotify-user-id"
```
## 🎨 Design Features
### 4. Spotify App Setup
1. Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard)
2. Create a new app
3. Add `http://localhost:3000/api/auth/callback/spotify` to Redirect URIs
4. Copy Client ID and Client Secret to your `.env.local`
- **Glassmorphism UI** - Modern frosted glass effects
- **Dark Mode** - Beautiful dark theme optimized for music
- **Fluid Animations** - Smooth transitions powered by Framer Motion
- **Responsive Design** - Works perfectly on all devices
- **Spotify Integration** - Seamless music playback and playlist creation
### 5. Installation & Setup
```bash
# Install dependencies
npm install
## 🛠 Tech Stack
# Generate Prisma client
npm run db:generate
- **Frontend:** React 18, TypeScript, Vite
- **Styling:** Tailwind CSS, Custom Glassmorphism
- **Animations:** Framer Motion
- **State Management:** Zustand
- **Music API:** Spotify Web API
- **Icons:** Lucide React
- **Notifications:** React Hot Toast
# Push database schema
npm run db:push
## 📱 Usage
# Start development server
npm run dev
```
1. **Connect Your Account:** Click "Connect with Spotify" and authorize the app
2. **Invite Your Partner:** Share the app with your partner to connect their account
3. **Explore Features:**
- **Last Listened:** See what each other is playing
- **Mixed Playlist:** Generate AI-powered playlists
- **Memory Lane:** Create and view shared musical memories
### 6. Get Spotify User IDs
To find your Spotify user IDs:
1. Go to your Spotify profile
2. Click "Share" → "Copy link to profile"
3. The user ID is in the URL: `https://open.spotify.com/user/{USER_ID}`
## 🔒 Privacy & Security
## Usage
- All data is stored locally in your browser
- No personal data is sent to external servers
- Spotify authentication is handled securely
- Playlist data is only shared between connected users
1. **Sign In**: Both users need to sign in with their Spotify accounts
2. **Dashboard**: View overview of both users' activity
3. **Timeline**: See shared listening history with heart markers for mutual songs
4. **Mix Generator**: Create AI-powered playlists based on both users' tastes
5. **Live Dashboard**: Real-time view of current listening activity and harmony matching
6. **Settings**: Customize your experience
## 🚀 Deployment
## API Endpoints
For production deployment:
- `GET /api/users` - Get all connected users
- `GET /api/timeline` - Get shared listening timeline
- `GET /api/spotify/recently-played` - Get user's recently played tracks
- `GET /api/spotify/currently-playing` - Get real-time listening data
- `POST /api/spotify/recommendations` - Generate music recommendations
- `POST /api/spotify/create-playlist` - Create Spotify playlist
- `POST /api/spotify/harmony` - Calculate harmony percentage
1. **Update environment variables** with production URLs
2. **Add production redirect URI** to Spotify app settings
3. **Build the app:**
```bash
npm run build
```
4. **Deploy** to your preferred hosting service (Vercel, Netlify, etc.)
## Security Features
## 💝 Made With Love
- User whitelist: Only specified Spotify users can access the app
- Secure token storage in PostgreSQL database
- Automatic token refresh handling
- Protected API routes with NextAuth session validation
This application was created as a special gift for someone special. Every feature is designed to bring two hearts closer through the universal language of music.
## Design Philosophy
## 📄 License
Harmony is designed with romance and elegance in mind:
- **Pastel color palette**: Rose, pink, and purple tones
- **Glassmorphism effects**: Beautiful frosted glass cards
- **Smooth animations**: Framer Motion for fluid interactions
- **Romantic touches**: Heart animations, floating elements, gradient text
- **Responsive design**: Works perfectly on all devices
## Contributing
This is a personal project, but feel free to fork and customize for your own use!
## License
MIT License - feel free to use this for your own romantic musical adventures! 💕
This project is private and personal. Please respect the privacy and don't redistribute.
---
Made with 💕 for couples who love music together
*"Music is the language of the spirit. It opens the secret of life bringing peace, abolishing strife." - Kahlil Gibran*

BIN
cloudflared-linux-amd64.deb Normal file

Binary file not shown.

Binary file not shown.

473
dist/assets/index-BailLCJ0.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/index-HYQ6lVkA.css vendored Normal file

File diff suppressed because one or more lines are too long

27
dist/callback.html vendored Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spotify Callback</title>
</head>
<body>
<script>
// Get the authorization code from URL parameters
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
// Redirect back to main app with error
window.location.href = 'http://localhost:3001/?error=' + encodeURIComponent(error);
} else if (code) {
// Redirect back to main app with code
window.location.href = 'http://localhost:3001/callback?code=' + encodeURIComponent(code);
} else {
// No code or error, redirect to main app
window.location.href = 'http://localhost:3001/';
}
</script>
</body>
</html>

3
dist/heart.svg vendored Normal file
View File

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 28C16 28 3 18 3 11C3 7.68629 5.68629 5 9 5C11.5 5 13.5 6.5 16 8.5C18.5 6.5 20.5 5 23 5C26.3137 5 29 7.68629 29 11C29 18 16 28 16 28Z" fill="#1db954"/>
</svg>

After

Width:  |  Height:  |  Size: 269 B

15
dist/index.html vendored Normal file
View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/heart.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>💕 Our Musical Journey</title>
<meta name="description" content="A private musical journey for two hearts connected through music" />
<script type="module" crossorigin src="/assets/index-BailLCJ0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-HYQ6lVkA.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

1
dist/placeholder-album.png vendored Normal file
View File

@ -0,0 +1 @@
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDMwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIiBmaWxsPSIjMjgyODI4Ii8+CjxjaXJjbGUgY3g9IjE1MCIgY3k9IjE1MCIgcj0iNTAiIGZpbGw9IiMxZGI5NTQiLz4KPHN2ZyB4PSIxMjUiIHk9IjEyNSIgd2lkdGg9IjUwIiBoZWlnaHQ9IjUwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiPgo8cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJTNi40OCAyMiAxMiAyMlMyMiAxNy41MiAyMiAxMlMxNy41MiAyIDEyIDJaTTEwIDE3TDUgMTJMMTAgN0wxMSAxMkwxNyAxMkwxMCAxN1oiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo8L3N2Zz4K

7
env.example Normal file
View File

@ -0,0 +1,7 @@
# Spotify API Configuration
VITE_SPOTIFY_CLIENT_ID=your_spotify_client_id_here
VITE_SPOTIFY_CLIENT_SECRET=your_spotify_client_secret_here
VITE_REDIRECT_URI=http://localhost:3000/callback
# Note: In production, make sure to add your production URL to the Spotify app settings
# VITE_REDIRECT_URI=https://yourdomain.com/callback

View File

@ -1,25 +0,0 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
];
export default eslintConfig;

30
frontend-dev.out Normal file
View File

@ -0,0 +1,30 @@
> spotify-couple-gift@0.0.0 dev
> vite
VITE v5.4.20 ready in 296 ms
➜ Local: http://localhost:3000/
➜ Network: http://159.195.9.107:3000/
1:41:33 PM [vite] page reload public/callback.html
1:41:54 PM [vite] page reload public/callback.html
1:42:05 PM [vite] page reload public/callback.html
1:43:55 PM [vite] hmr update /src/pages/CallbackPage.tsx, /src/styles/globals.css
1:44:09 PM [vite] hmr update /src/pages/CallbackPage.tsx, /src/styles/globals.css
1:44:25 PM [vite] hmr update /src/pages/CallbackPage.tsx, /src/styles/globals.css
1:45:56 PM [vite] hmr update /src/pages/LoginPage.tsx, /src/styles/globals.css, /src/pages/LastListenedPage.tsx, /src/pages/MixedPlaylistPage.tsx
1:46:02 PM [vite] hmr update /src/pages/CallbackPage.tsx, /src/styles/globals.css
1:46:20 PM [vite] hmr update /src/pages/CallbackPage.tsx, /src/styles/globals.css
1:46:24 PM [vite] hmr update /src/pages/LoginPage.tsx, /src/styles/globals.css, /src/pages/LastListenedPage.tsx, /src/pages/MixedPlaylistPage.tsx
1:46:43 PM [vite] hmr update /src/pages/CallbackPage.tsx, /src/styles/globals.css
1:46:49 PM [vite] hmr update /src/pages/CallbackPage.tsx, /src/styles/globals.css
1:47:43 PM [vite] hmr update /src/pages/LoginPage.tsx, /src/styles/globals.css, /src/pages/LastListenedPage.tsx, /src/pages/MixedPlaylistPage.tsx
1:47:43 PM [vite] hmr update /src/pages/CallbackPage.tsx, /src/styles/globals.css
1:49:50 PM [vite] page reload public/callback.html
1:50:15 PM [vite] page reload public/callback.html
1:51:31 PM [vite] hmr update /src/pages/MixedPlaylistPage.tsx, /src/styles/globals.css
1:51:32 PM [vite] hmr update /src/pages/LoginPage.tsx, /src/styles/globals.css, /src/pages/LastListenedPage.tsx, /src/pages/MixedPlaylistPage.tsx
1:51:32 PM [vite] hmr update /src/App.tsx, /src/styles/globals.css
1:51:33 PM [vite] page reload public/callback.html
1:51:33 PM [vite] hmr update /src/pages/CallbackPage.tsx, /src/styles/globals.css

1
frontend-dev.pid Normal file
View File

@ -0,0 +1 @@
589178

14
index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/heart.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>💕 Our Musical Journey</title>
<meta name="description" content="A private musical journey for two hearts connected through music" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

BIN
mkcert-v1.4.4-linux-amd64 Executable file

Binary file not shown.

View File

@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

BIN
ngrok Executable file

Binary file not shown.

8383
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,43 @@
{
"name": "harmony",
"version": "0.1.0",
"name": "spotify-couple-gift",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio"
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@prisma/client": "^6.17.0",
"@types/spotify-web-api-node": "^5.0.11",
"framer-motion": "^12.23.22",
"lucide-react": "^0.545.0",
"next": "15.5.4",
"next-auth": "^4.24.11",
"prisma": "^6.17.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"spotify-web-api-node": "^5.0.2"
"clsx": "^2.0.0",
"framer-motion": "^10.16.16",
"gsap": "^3.13.0",
"http-proxy-middleware": "^3.0.5",
"lucide-react": "^0.294.0",
"motion": "^12.23.24",
"ogl": "^1.0.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-router-dom": "^6.20.1",
"spotify-web-api-js": "^1.5.2",
"tailwind-merge": "^2.0.0",
"zustand": "^4.4.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.5.4",
"tailwindcss": "^4",
"typescript": "^5"
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.1.1",
"autoprefixer": "^10.4.16",
"eslint": "^8.53.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,5 +0,0 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

2
preview-3443.out Normal file
View File

@ -0,0 +1,2 @@
(node:612693) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated
(Use `node --trace-deprecation ...` to show where the warning was created)

View File

@ -1,132 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
spotifyId String @unique
displayName String
email String
profileImage String?
accessToken String
refreshToken String
tokenExpiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relationships
recentlyPlayed RecentlyPlayed[]
currentlyPlaying CurrentlyPlaying[]
topTracks TopTrack[]
topArtists TopArtist[]
playlists Playlist[]
@@map("users")
}
model RecentlyPlayed {
id String @id @default(cuid())
userId String
trackId String
trackName String
artistName String
albumName String
albumImage String?
playedAt DateTime
duration Int
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, trackId, playedAt])
@@map("recently_played")
}
model CurrentlyPlaying {
id String @id @default(cuid())
userId String
trackId String?
trackName String?
artistName String?
albumName String?
albumImage String?
isPlaying Boolean @default(false)
progressMs Int?
durationMs Int?
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("currently_playing")
}
model TopTrack {
id String @id @default(cuid())
userId String
trackId String
trackName String
artistName String
albumName String
albumImage String?
popularity Int
timeRange String // short_term, medium_term, long_term
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("top_tracks")
}
model TopArtist {
id String @id @default(cuid())
userId String
artistId String
artistName String
artistImage String?
popularity Int
timeRange String // short_term, medium_term, long_term
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("top_artists")
}
model Playlist {
id String @id @default(cuid())
userId String
spotifyId String? // Spotify playlist ID if created
name String
description String?
imageUrl String?
isShared Boolean @default(false)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tracks PlaylistTrack[]
@@map("playlists")
}
model PlaylistTrack {
id String @id @default(cuid())
playlistId String
trackId String
trackName String
artistName String
albumName String
albumImage String?
addedAt DateTime @default(now())
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
@@map("playlist_tracks")
}

BIN
prod.out Normal file

Binary file not shown.

1
prod.pid Normal file
View File

@ -0,0 +1 @@
631996

127
production-server.js Normal file
View File

@ -0,0 +1,127 @@
import https from 'https';
import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const options = {
key: fs.readFileSync('./localhost+2-key.pem'),
cert: fs.readFileSync('./localhost+2.pem')
};
// MIME types mapping
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.htm': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.mjs': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject'
};
const API_TARGET = process.env.API_TARGET || 'http://127.0.0.1:8081';
const server = https.createServer(options, (req, res) => {
console.log(`📥 ${req.method} ${req.url}`);
// Reverse proxy for API
if (req.url && req.url.startsWith('/api')) {
const targetUrl = new URL(API_TARGET);
const proxiedPath = req.url.replace(/^\/api/, '') || '/';
// Avoid forwarding content-length; let Node set it
const headers = { ...req.headers };
delete headers['content-length'];
delete headers['host'];
const reqOpts = {
hostname: targetUrl.hostname,
port: targetUrl.port || 80,
path: proxiedPath,
method: req.method,
headers,
};
const proxy = http.request(reqOpts, (pres) => {
res.writeHead(pres.statusCode || 500, pres.headers);
pres.pipe(res);
});
proxy.on('error', (err) => {
console.error('Proxy error:', err);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxy);
return;
}
let filePath = path.join(__dirname, 'dist', req.url === '/' ? 'index.html' : req.url);
// Security check - prevent directory traversal
if (!filePath.startsWith(path.join(__dirname, 'dist'))) {
res.writeHead(403);
res.end('Forbidden');
return;
}
// Check if file exists
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
// File not found, serve index.html for SPA routing
const indexPath = path.join(__dirname, 'dist', 'index.html');
fs.readFile(indexPath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('File not found');
return;
}
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-cache'
});
res.end(data);
});
return;
}
// Read and serve the file
fs.readFile(filePath, (err, data) => {
if (err) {
console.error('Error reading file:', err);
res.writeHead(500);
res.end('Server Error');
return;
}
const ext = path.extname(filePath);
const contentType = mimeTypes[ext] || 'application/octet-stream';
// Set cache headers for static assets
const cacheControl = ext.match(/\.(css|js|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf)$/)
? 'public, max-age=31536000'
: 'no-cache';
res.writeHead(200, {
'Content-Type': contentType,
'Cache-Control': cacheControl
});
res.end(data);
});
});
});
server.listen(3443, '0.0.0.0', () => {
console.log('🚀 Production HTTPS Server running on https://159.195.9.107:3443');
console.log('📁 Serving files from: ./dist/');
console.log('🔒 SSL Certificate: localhost+2.pem');
});

27
public/callback.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spotify Callback</title>
</head>
<body>
<script>
// Get the authorization code from URL parameters
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
// Redirect back to main app with error
window.location.href = 'http://localhost:3001/?error=' + encodeURIComponent(error);
} else if (code) {
// Redirect back to main app with code
window.location.href = 'http://localhost:3001/callback?code=' + encodeURIComponent(code);
} else {
// No code or error, redirect to main app
window.location.href = 'http://localhost:3001/';
}
</script>
</body>
</html>

View File

@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

3
public/heart.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 28C16 28 3 18 3 11C3 7.68629 5.68629 5 9 5C11.5 5 13.5 6.5 16 8.5C18.5 6.5 20.5 5 23 5C26.3137 5 29 7.68629 29 11C29 18 16 28 16 28Z" fill="#1db954"/>
</svg>

After

Width:  |  Height:  |  Size: 269 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDMwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIiBmaWxsPSIjMjgyODI4Ii8+CjxjaXJjbGUgY3g9IjE1MCIgY3k9IjE1MCIgcj0iNTAiIGZpbGw9IiMxZGI5NTQiLz4KPHN2ZyB4PSIxMjUiIHk9IjEyNSIgd2lkdGg9IjUwIiBoZWlnaHQ9IjUwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiPgo8cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJTNi40OCAyMiAxMiAyMlMyMiAxNy41MiAyMiAxMlMxNy41MiAyIDEyIDJaTTEwIDE3TDUgMTJMMTAgN0wxMSAxMkwxNyAxMkwxMCAxN1oiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo8L3N2Zz4K

View File

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

882
server-dev.out Normal file
View File

@ -0,0 +1,882 @@
> spotify-backend@0.1.0 dev
> tsx watch src/index.ts
API server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
POST /auth/exchange 200 144.220 ms - 749
POST /users/91416peucsefexd7z3q9875hw/sync 200 349.388 ms - 11
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 12.725 ms - 156666
GET /users/91416peucsefexd7z3q9875hw 200 1.174 ms - 232
GET /users/91416peucsefexd7z3q9875hw/top-tracks?time_range=short_term 200 4.523 ms - 73042
GET /partners/partner/91416peucsefexd7z3q9875hw 200 3.006 ms - 44
GET /users/31at7552nbs34vogjfzmasypv6vy 200 0.816 ms - 238
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 7.860 ms - 156852
GET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term 200 3.907 ms - 61623
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 9.232 ms - 156666
GET /partners/requests/91416peucsefexd7z3q9875hw 200 11.611 ms - 2
GET /users/91416peucsefexd7z3q9875hw/status 200 1.466 ms - 57
GET /users/91416peucsefexd7z3q9875hw/status 200 0.873 ms - 57
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 8.504 ms - 156852
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 35.615 ms - 20
GET /users/31at7552nbs34vogjfzmasypv6vy/now-playing 200 3.749 ms - 20
GET /playlists/mixed 200 2.786 ms - 5029
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg 200 3.610 ms - 264761
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg 200 2.557 ms - 144313
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg 200 2.834 ms - 204401
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg 200 5.850 ms - 264761
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg 200 3.053 ms - 144313
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg 200 3.028 ms - 204401
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg 200 6.302 ms - 264761
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg 200 9.533 ms - 144313
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg 200 12.364 ms - 204401
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg 200 5.427 ms - 264761
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg 200 9.141 ms - 204401
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg 200 2.173 ms - 144313
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg 200 3.012 ms - 264761
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg 200 3.370 ms - 144313
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg 200 2.712 ms - 204401
Selected random playlist cover: /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg
Saving playlist to database: {
id: '008255d7-98e8-493d-8bfd-659b0bdcbd09',
name: 'Chill Melody - Oct 16',
trackCount: 25,
trackUris: [
'spotify:track:1FywSVKhuVzqVyuJrqDo2E',
'spotify:track:5WwhpsQ9ARJEyCEZqyP15E',
'spotify:track:0BVQNP096tuY7s9ggvtEOo'
]
}
POST /playlists/mixed 200 1772.528 ms - 1400
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg 200 6.630 ms - 264761
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg 200 11.088 ms - 204401
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 12.794 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg 200 6.741 ms - 144313
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg 200 3.646 ms - 204401
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 3.497 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg 200 6.103 ms - 144313
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg 200 4.945 ms - 264761
DELETE /playlists/mixed/a8580fac-781a-472e-94ce-ac88e7f67cb4 200 2.750 ms - 16
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg 200 3.500 ms - 204401
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 2.855 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg 200 5.074 ms - 144313
DELETE /playlists/mixed/ac740cb2-3d6b-4a03-94d1-7c5bbb4514fa 200 2.122 ms - 16
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 3.704 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg 200 6.880 ms - 204401
DELETE /playlists/mixed/21f99970-416e-474a-8819-aaab31afa649 200 2.111 ms - 16
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 2.857 ms - 132357
DELETE /playlists/mixed/008255d7-98e8-493d-8bfd-659b0bdcbd09 200 1.747 ms - 16
2:06:12 PM [tsx] change in ./src/lib/spotify.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:06:30 PM [tsx] change in ./src/lib/spotify.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:06:49 PM [tsx] change in ./src/lib/spotify.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:06:54 PM [tsx] change in ./src/routes/playlists.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:07:04 PM [tsx] change in ./src/routes/playlists.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:07:18 PM [tsx] change in ./src/routes/playlists.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:07:33 PM [tsx] change in ./src/routes/playlists.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:07:50 PM [tsx] change in ./src/lib/spotify.ts Restarting...
c2:07:51 PM [tsx] change in ./src/routes/playlists.ts Restarting...
API server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:08:00 PM [tsx] change in ./src/routes/playlists.ts Restarting...
c2:08:00 PM [tsx] change in ./src/routes/playlists.ts Restarting...
API server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:08:11 PM [tsx] change in ./src/routes/playlists.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:08:15 PM [tsx] change in ./src/routes/playlists.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:08:25 PM [tsx] change in ./src/routes/playlists.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:08:42 PM [tsx] change in ./src/routes/playlists.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:08:56 PM [tsx] change in ./src/lib/spotify.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
2:10:23 PM [tsx] change in ./src/routes/playlists.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
GET / 404 4.074 ms - 139
2:10:52 PM [tsx] change in ./src/lib/spotify.ts Restarting...
cAPI server (HTTP) listening on http://0.0.0.0:8081
API server (HTTPS) listening on https://0.0.0.0:8082
GET /partners/partner/91416peucsefexd7z3q9875hw 200 10.347 ms - 44
GET /playlists/mixed 200 3.149 ms - 16
GET /partners/partner/91416peucsefexd7z3q9875hw 200 2.377 ms - 44
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 7.331 ms - 156666
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 5.987 ms - 156852
GET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term 200 3.810 ms - 61623
GET /partners/requests/91416peucsefexd7z3q9875hw 200 2.445 ms - 2
GET /users/91416peucsefexd7z3q9875hw/status 200 1.295 ms - 57
GET /users/31at7552nbs34vogjfzmasypv6vy 200 2.151 ms - 238
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 7.602 ms - 156852
GET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term 200 2.906 ms - 61623
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 98.682 ms - 20
GET /users/91416peucsefexd7z3q9875hw/status 200 7.454 ms - 57
GET /users/31at7552nbs34vogjfzmasypv6vy 200 1.046 ms - 238
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 5.938 ms - 156666
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 7.101 ms - 156852
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 44.738 ms - 20
GET /users/31at7552nbs34vogjfzmasypv6vy/now-playing 200 2.759 ms - 20
GET /playlists/mixed 200 2.235 ms - 16
[MIXED PLAYLIST] Starting playlist creation process for creator: 91416peucsefexd7z3q9875hw, partner: 31at7552nbs34vogjfzmasypv6vy
[MIXED PLAYLIST] Request parameters: {
name: undefined,
description: 'An AI-blended mix with fresh recommendations',
vibe: undefined,
genres: [ 'pop' ],
includeKnown: true,
createForBoth: false,
limit: 25
}
[MIXED PLAYLIST] Collecting seed tracks from creator (91416peucsefexd7z3q9875hw) and partner (31at7552nbs34vogjfzmasypv6vy)
[MIXED PLAYLIST] Found 20 tracks from creator, 16 tracks from partner
[MIXED PLAYLIST] Total seed track URIs: 36
[MIXED PLAYLIST] Creating Spotify playlist for creator: "Ethereal Symphony - Oct 16"
[PLAYLIST CREATION] Creating Spotify playlist: "Ethereal Symphony - Oct 16" for user: 91416peucsefexd7z3q9875hw
[PLAYLIST CREATION] Spotify playlist created successfully with ID: 0aUuc9FNcFaIFMuQm2tXdZ
[PLAYLIST COVER] Looking for images in directory: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers
[PLAYLIST COVER] Found 14 image files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg',
'cover1.svg',
'cover2.svg',
'cover3.svg',
'cover4.svg',
'cover5.svg'
]
[PLAYLIST COVER] Selected random playlist cover: /api/playlist-covers/cover1.svg (from file: cover1.svg)
[PLAYLIST COVER] Looking for file: cover1.svg in files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg',
'cover1.svg',
'cover2.svg',
'cover3.svg',
'cover4.svg',
'cover5.svg'
]
[PLAYLIST COVER] Attempting to upload cover image: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/cover1.svg
[PLAYLIST COVER] Attempting to upload cover image for playlist 0aUuc9FNcFaIFMuQm2tXdZ from path: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/cover1.svg
[PLAYLIST COVER] Image file size: 520 bytes (0.51 KB)
[PLAYLIST COVER] Failed to upload cover image for playlist 0aUuc9FNcFaIFMuQm2tXdZ: { error: { status: 401, message: 'Unauthorized.' } }
[PLAYLIST COVER] Failed to upload cover image to Spotify playlist 0aUuc9FNcFaIFMuQm2tXdZ, but playlist was created successfully
[MIXED PLAYLIST] Adding 25 tracks to creator playlist 0aUuc9FNcFaIFMuQm2tXdZ
[MIXED PLAYLIST] Successfully added tracks to creator playlist
[MIXED PLAYLIST] Saving playlist to database: {
id: 'f9d6bf45-4b8f-472a-b482-e66fd88a3e16',
name: 'Ethereal Symphony - Oct 16',
trackCount: 25,
creatorPlaylistId: '0aUuc9FNcFaIFMuQm2tXdZ',
partnerPlaylistId: undefined,
imageUrl: '/api/playlist-covers/cover1.svg',
trackUris: [
'spotify:track:2374M0fQpWi3dLnB54qaLX',
'spotify:track:3siwsiaEoU4Kuuc9WKMUy5',
'spotify:track:5DxDLsW6PsLz5gkwC7Mk5S'
]
}
[MIXED PLAYLIST] Playlist creation completed successfully: {
id: 'f9d6bf45-4b8f-472a-b482-e66fd88a3e16',
name: 'Ethereal Symphony - Oct 16',
creatorPlaylistId: '0aUuc9FNcFaIFMuQm2tXdZ',
partnerPlaylistId: undefined,
trackCount: 25,
imageUrl: '/api/playlist-covers/cover1.svg'
}
POST /playlists/mixed 200 1429.828 ms - 1359
GET /api/playlist-covers/cover1.svg 200 3.533 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.969 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.872 ms - 736
GET /api/playlist-covers/cover1.svg 200 1.197 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.691 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.926 ms - 736
GET /api/playlist-covers/cover1.svg 200 1.094 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.750 ms - 736
[MIXED PLAYLIST] Starting playlist creation process for creator: 91416peucsefexd7z3q9875hw, partner: 31at7552nbs34vogjfzmasypv6vy
[MIXED PLAYLIST] Request parameters: {
name: undefined,
description: 'An AI-blended mix with fresh recommendations',
vibe: undefined,
genres: [ 'edm' ],
includeKnown: true,
createForBoth: false,
limit: 25
}
[MIXED PLAYLIST] Collecting seed tracks from creator (91416peucsefexd7z3q9875hw) and partner (31at7552nbs34vogjfzmasypv6vy)
[MIXED PLAYLIST] Found 20 tracks from creator, 16 tracks from partner
[MIXED PLAYLIST] Total seed track URIs: 36
GET /api/playlist-covers/cover1.svg 200 0.638 ms - 736
[MIXED PLAYLIST] Creating Spotify playlist for creator: "Electric Sound - Oct 16"
[PLAYLIST CREATION] Creating Spotify playlist: "Electric Sound - Oct 16" for user: 91416peucsefexd7z3q9875hw
[PLAYLIST CREATION] Spotify playlist created successfully with ID: 2PSoapi3TkMSQxQeJ1wEfy
[PLAYLIST COVER] Looking for images in directory: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers
[PLAYLIST COVER] Found 14 image files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg',
'cover1.svg',
'cover2.svg',
'cover3.svg',
'cover4.svg',
'cover5.svg'
]
[PLAYLIST COVER] Selected random playlist cover: /api/playlist-covers/cover1.svg (from file: cover1.svg)
[PLAYLIST COVER] Looking for file: cover1.svg in files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg',
'cover1.svg',
'cover2.svg',
'cover3.svg',
'cover4.svg',
'cover5.svg'
]
[PLAYLIST COVER] Attempting to upload cover image: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/cover1.svg
[PLAYLIST COVER] Attempting to upload cover image for playlist 2PSoapi3TkMSQxQeJ1wEfy from path: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/cover1.svg
[PLAYLIST COVER] Image file size: 520 bytes (0.51 KB)
[PLAYLIST COVER] Failed to upload cover image for playlist 2PSoapi3TkMSQxQeJ1wEfy: { error: { status: 401, message: 'Unauthorized.' } }
[PLAYLIST COVER] Failed to upload cover image to Spotify playlist 2PSoapi3TkMSQxQeJ1wEfy, but playlist was created successfully
[MIXED PLAYLIST] Adding 25 tracks to creator playlist 2PSoapi3TkMSQxQeJ1wEfy
[MIXED PLAYLIST] Successfully added tracks to creator playlist
[MIXED PLAYLIST] Saving playlist to database: {
id: '8ec2d0d9-de93-4ee7-ab76-69dc6ea6b432',
name: 'Electric Sound - Oct 16',
trackCount: 25,
creatorPlaylistId: '2PSoapi3TkMSQxQeJ1wEfy',
partnerPlaylistId: undefined,
imageUrl: '/api/playlist-covers/cover1.svg',
trackUris: [
'spotify:track:5WwhpsQ9ARJEyCEZqyP15E',
'spotify:track:6jDJVFA6A77yJNKR6UJvZo',
'spotify:track:2GgE1PFCZGjViqHjQUpYkz'
]
}
[MIXED PLAYLIST] Playlist creation completed successfully: {
id: '8ec2d0d9-de93-4ee7-ab76-69dc6ea6b432',
name: 'Electric Sound - Oct 16',
creatorPlaylistId: '2PSoapi3TkMSQxQeJ1wEfy',
partnerPlaylistId: undefined,
trackCount: 25,
imageUrl: '/api/playlist-covers/cover1.svg'
}
POST /playlists/mixed 200 1239.488 ms - 1356
GET /api/playlist-covers/cover1.svg 200 0.613 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.759 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.689 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.667 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.969 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.757 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.999 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.927 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.763 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.873 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.677 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.809 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.867 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.766 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.803 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.678 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.800 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.764 ms - 736
[MIXED PLAYLIST] Starting playlist creation process for creator: 91416peucsefexd7z3q9875hw, partner: 31at7552nbs34vogjfzmasypv6vy
[MIXED PLAYLIST] Request parameters: {
name: undefined,
description: 'An AI-blended mix with fresh recommendations',
vibe: undefined,
genres: [ 'rock' ],
includeKnown: true,
createForBoth: false,
limit: 25
}
[MIXED PLAYLIST] Collecting seed tracks from creator (91416peucsefexd7z3q9875hw) and partner (31at7552nbs34vogjfzmasypv6vy)
[MIXED PLAYLIST] Found 20 tracks from creator, 16 tracks from partner
[MIXED PLAYLIST] Total seed track URIs: 36
GET /api/playlist-covers/cover1.svg 200 0.874 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.756 ms - 736
[MIXED PLAYLIST] Creating Spotify playlist for creator: "Harmonic Tune - Oct 16"
[PLAYLIST CREATION] Creating Spotify playlist: "Harmonic Tune - Oct 16" for user: 91416peucsefexd7z3q9875hw
[PLAYLIST CREATION] Spotify playlist created successfully with ID: 53jyCWUlxwsRgl7v6gCmzo
[PLAYLIST COVER] Looking for images in directory: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers
[PLAYLIST COVER] Found 14 image files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg',
'cover1.svg',
'cover2.svg',
'cover3.svg',
'cover4.svg',
'cover5.svg'
]
[PLAYLIST COVER] Selected random playlist cover: /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg (from file: WhatsApp Image 2025-10-16 at 11.12.30.jpeg)
[PLAYLIST COVER] Looking for file: WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg in files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg',
'cover1.svg',
'cover2.svg',
'cover3.svg',
'cover4.svg',
'cover5.svg'
]
[PLAYLIST COVER] Attempting to upload cover image: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.30.jpeg
[PLAYLIST COVER] Attempting to upload cover image for playlist 53jyCWUlxwsRgl7v6gCmzo from path: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.30.jpeg
[PLAYLIST COVER] Image file size: 98769 bytes (96.45 KB)
[PLAYLIST COVER] Failed to upload cover image for playlist 53jyCWUlxwsRgl7v6gCmzo: { error: { status: 401, message: 'Unauthorized.' } }
[PLAYLIST COVER] Failed to upload cover image to Spotify playlist 53jyCWUlxwsRgl7v6gCmzo, but playlist was created successfully
[MIXED PLAYLIST] Adding 25 tracks to creator playlist 53jyCWUlxwsRgl7v6gCmzo
[MIXED PLAYLIST] Successfully added tracks to creator playlist
[MIXED PLAYLIST] Saving playlist to database: {
id: '8791c148-a234-4e08-8ff2-04f54f4348a1',
name: 'Harmonic Tune - Oct 16',
trackCount: 25,
creatorPlaylistId: '53jyCWUlxwsRgl7v6gCmzo',
partnerPlaylistId: undefined,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg',
trackUris: [
'spotify:track:2zYzyRzz6pRmhPzyfMEC8s',
'spotify:track:6QAsrXPnMSXIbV0yEJHlEX',
'spotify:track:4Yf5bqU3NK4kNOypcrLYwU'
]
}
[MIXED PLAYLIST] Playlist creation completed successfully: {
id: '8791c148-a234-4e08-8ff2-04f54f4348a1',
name: 'Harmonic Tune - Oct 16',
creatorPlaylistId: '53jyCWUlxwsRgl7v6gCmzo',
partnerPlaylistId: undefined,
trackCount: 25,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg'
}
POST /playlists/mixed 200 1497.571 ms - 1395
GET /api/playlist-covers/cover1.svg 200 1.352 ms - 736
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 5.426 ms - 131729
GET /api/playlist-covers/cover1.svg 200 0.694 ms - 736
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.116 ms - 131729
GET /api/playlist-covers/cover1.svg 200 0.757 ms - 736
GET /api/playlist-covers/cover1.svg 200 0.685 ms - 736
GET /api/playlist-covers/cover1.svg 404 0.725 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.131 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.736 ms - 27
GET /api/playlist-covers/cover1.svg 404 0.627 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.331 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.537 ms - 27
GET /api/playlist-covers/cover1.svg 404 0.683 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 1.895 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.571 ms - 27
GET /api/playlist-covers/cover1.svg 404 1.176 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.786 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.588 ms - 27
GET /api/playlist-covers/cover1.svg 404 1.487 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 3.403 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.698 ms - 27
GET /api/playlist-covers/cover1.svg 404 0.659 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 3.457 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.695 ms - 27
GET /api/playlist-covers/cover1.svg 404 1.205 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 3.829 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.572 ms - 27
GET /api/playlist-covers/cover1.svg 404 0.953 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.118 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.782 ms - 27
GET /api/playlist-covers/cover1.svg 404 1.583 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 4.709 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.857 ms - 27
[MIXED PLAYLIST] Starting playlist creation process for creator: 91416peucsefexd7z3q9875hw, partner: 31at7552nbs34vogjfzmasypv6vy
[MIXED PLAYLIST] Request parameters: {
name: undefined,
description: 'An AI-blended mix with fresh recommendations',
vibe: undefined,
genres: [ 'indie' ],
includeKnown: true,
createForBoth: false,
limit: 25
}
[MIXED PLAYLIST] Collecting seed tracks from creator (91416peucsefexd7z3q9875hw) and partner (31at7552nbs34vogjfzmasypv6vy)
[MIXED PLAYLIST] Found 20 tracks from creator, 16 tracks from partner
[MIXED PLAYLIST] Total seed track URIs: 36
GET /api/playlist-covers/cover1.svg 404 1.120 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 3.859 ms - 131729
GET /api/playlist-covers/cover1.svg 404 1.464 ms - 27
[MIXED PLAYLIST] Creating Spotify playlist for creator: "Dreamy Blend - Oct 16"
[PLAYLIST CREATION] Creating Spotify playlist: "Dreamy Blend - Oct 16" for user: 91416peucsefexd7z3q9875hw
[PLAYLIST CREATION] Spotify playlist created successfully with ID: 3cNxtBOsaZdJyWo2DGytkJ
[PLAYLIST COVER] Looking for images in directory: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers
[PLAYLIST COVER] Found 9 image files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg'
]
[PLAYLIST COVER] Selected random playlist cover: /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg (from file: WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg)
[PLAYLIST COVER] Looking for file: WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg in files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg'
]
[PLAYLIST COVER] Attempting to upload cover image: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg
[PLAYLIST COVER] Attempting to upload cover image for playlist 3cNxtBOsaZdJyWo2DGytkJ from path: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg
[PLAYLIST COVER] Image file size: 99239 bytes (96.91 KB)
[PLAYLIST COVER] Failed to upload cover image for playlist 3cNxtBOsaZdJyWo2DGytkJ: { error: { status: 401, message: 'Unauthorized.' } }
[PLAYLIST COVER] Failed to upload cover image to Spotify playlist 3cNxtBOsaZdJyWo2DGytkJ, but playlist was created successfully
[MIXED PLAYLIST] Adding 25 tracks to creator playlist 3cNxtBOsaZdJyWo2DGytkJ
[MIXED PLAYLIST] Successfully added tracks to creator playlist
[MIXED PLAYLIST] Saving playlist to database: {
id: 'f66b57ab-0baa-4740-9303-fedbc5b90df6',
name: 'Dreamy Blend - Oct 16',
trackCount: 25,
creatorPlaylistId: '3cNxtBOsaZdJyWo2DGytkJ',
partnerPlaylistId: undefined,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg',
trackUris: [
'spotify:track:0BxE4FqsDD1Ot4YuBXwAPp',
'spotify:track:20B2I2t11yceg5v6CSdF2C',
'spotify:track:1v1cijv1qjMJ5o9OvMWACS'
]
}
[MIXED PLAYLIST] Playlist creation completed successfully: {
id: 'f66b57ab-0baa-4740-9303-fedbc5b90df6',
name: 'Dreamy Blend - Oct 16',
creatorPlaylistId: '3cNxtBOsaZdJyWo2DGytkJ',
partnerPlaylistId: undefined,
trackCount: 25,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg'
}
POST /playlists/mixed 200 1488.677 ms - 1400
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.880 ms - 131729
GET /api/playlist-covers/cover1.svg 404 3.061 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 1.993 ms - 132357
GET /api/playlist-covers/cover1.svg 404 0.668 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 1.960 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.936 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 1.543 ms - 132357
GET /api/playlist-covers/cover1.svg 404 0.644 ms - 27
GET /partners/partner/91416peucsefexd7z3q9875hw 200 2.126 ms - 44
GET /playlists/mixed 200 2.656 ms - 6620
GET /partners/partner/91416peucsefexd7z3q9875hw 200 2.145 ms - 44
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 5.171 ms - 156666
GET /partners/requests/91416peucsefexd7z3q9875hw 200 3.568 ms - 2
GET /users/91416peucsefexd7z3q9875hw/status 200 0.821 ms - 57
GET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term 200 2.420 ms - 61623
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 4.760 ms - 156852
GET /users/31at7552nbs34vogjfzmasypv6vy 200 0.684 ms - 238
GET /users/91416peucsefexd7z3q9875hw/status 200 0.511 ms - 57
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 4.954 ms - 156852
GET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term 200 4.000 ms - 61623
GET /users/31at7552nbs34vogjfzmasypv6vy 200 4.409 ms - 238
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 6.305 ms - 156666
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 4.941 ms - 156852
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 331.408 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 386.557 ms - 20
GET /users/31at7552nbs34vogjfzmasypv6vy/now-playing 200 2.108 ms - 20
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 4.406 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 6.255 ms - 131729
GET /api/playlist-covers/cover1.svg 404 6.573 ms - 27
GET /playlists/mixed 200 2.180 ms - 6620
GET /api/playlist-covers/cover1.svg 404 0.499 ms - 27
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 3.759 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 5.424 ms - 131729
GET /api/playlist-covers/cover1.svg 404 0.527 ms - 27
GET /api/playlist-covers/cover1.svg 404 0.985 ms - 27
DELETE /playlists/mixed/8ec2d0d9-de93-4ee7-ab76-69dc6ea6b432 200 1.857 ms - 16
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 3.592 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.204 ms - 131729
GET /api/playlist-covers/cover1.svg 404 2.529 ms - 27
DELETE /playlists/mixed/f9d6bf45-4b8f-472a-b482-e66fd88a3e16 200 1.599 ms - 16
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 2.167 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 5.011 ms - 131729
GET /partners/partner/91416peucsefexd7z3q9875hw 200 1.746 ms - 44
GET /playlists/mixed 200 2.243 ms - 3359
GET /partners/partner/91416peucsefexd7z3q9875hw 200 3.642 ms - 44
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 7.241 ms - 156666
GET /partners/requests/91416peucsefexd7z3q9875hw 200 1.888 ms - 2
GET /users/91416peucsefexd7z3q9875hw/status 200 0.669 ms - 57
GET /users/31at7552nbs34vogjfzmasypv6vy 200 0.909 ms - 238
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 6.056 ms - 156852
GET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term 200 2.810 ms - 61623
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 53.602 ms - 20
GET /users/31at7552nbs34vogjfzmasypv6vy 200 0.725 ms - 238
GET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term 200 2.653 ms - 61623
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 6.048 ms - 156852
GET /users/91416peucsefexd7z3q9875hw/status 200 0.730 ms - 57
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 5.416 ms - 156666
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 4.647 ms - 156852
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 44.441 ms - 20
GET /users/31at7552nbs34vogjfzmasypv6vy/now-playing 200 2.167 ms - 20
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 2.594 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 4.424 ms - 131729
GET /playlists/mixed 200 5.963 ms - 3359
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 2.812 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 1.637 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 1.989 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.290 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 3.188 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.014 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 3.943 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 6.145 ms - 131729
[MIXED PLAYLIST] Starting playlist creation process for creator: 91416peucsefexd7z3q9875hw, partner: 31at7552nbs34vogjfzmasypv6vy
[MIXED PLAYLIST] Request parameters: {
name: undefined,
description: 'An AI-blended mix with fresh recommendations',
vibe: undefined,
genres: [ 'pop' ],
includeKnown: true,
createForBoth: false,
limit: 25
}
[MIXED PLAYLIST] Collecting seed tracks from creator (91416peucsefexd7z3q9875hw) and partner (31at7552nbs34vogjfzmasypv6vy)
[MIXED PLAYLIST] Found 20 tracks from creator, 16 tracks from partner
[MIXED PLAYLIST] Total seed track URIs: 36
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.525 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 2.561 ms - 132357
[MIXED PLAYLIST] Creating Spotify playlist for creator: "Rhythmic Sound - Oct 16"
[PLAYLIST CREATION] Creating Spotify playlist: "Rhythmic Sound - Oct 16" for user: 91416peucsefexd7z3q9875hw
[PLAYLIST CREATION] Spotify playlist created successfully with ID: 0st3z31nDWsY5T4oRMsZwF
[PLAYLIST COVER] Looking for images in directory: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers
[PLAYLIST COVER] Found 9 image files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg'
]
[PLAYLIST COVER] Selected random playlist cover: /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg (from file: WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg)
[PLAYLIST COVER] Looking for file: WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg in files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg'
]
[PLAYLIST COVER] Attempting to upload cover image: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg
[PLAYLIST COVER] Attempting to upload cover image for playlist 0st3z31nDWsY5T4oRMsZwF from path: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg
[PLAYLIST COVER] Image file size: 203140 bytes (198.38 KB)
[PLAYLIST COVER] Failed to upload cover image for playlist 0st3z31nDWsY5T4oRMsZwF: Request failed with status code 413
[PLAYLIST COVER] Failed to upload cover image to Spotify playlist 0st3z31nDWsY5T4oRMsZwF, but playlist was created successfully
[MIXED PLAYLIST] Adding 25 tracks to creator playlist 0st3z31nDWsY5T4oRMsZwF
[MIXED PLAYLIST] Successfully added tracks to creator playlist
[MIXED PLAYLIST] Saving playlist to database: {
id: '8bba02a1-389e-46e8-b4a2-efbb016647a3',
name: 'Rhythmic Sound - Oct 16',
trackCount: 25,
creatorPlaylistId: '0st3z31nDWsY5T4oRMsZwF',
partnerPlaylistId: undefined,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg',
trackUris: [
'spotify:track:6MrLkXsMmHaYt680fhJUAq',
'spotify:track:5BZsQlgw21vDOAjoqkNgKb',
'spotify:track:6dOtVTDdiauQNBQEDOtlAB'
]
}
[MIXED PLAYLIST] Playlist creation completed successfully: {
id: '8bba02a1-389e-46e8-b4a2-efbb016647a3',
name: 'Rhythmic Sound - Oct 16',
creatorPlaylistId: '0st3z31nDWsY5T4oRMsZwF',
partnerPlaylistId: undefined,
trackCount: 25,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg'
}
POST /playlists/mixed 200 1309.712 ms - 1402
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 4.178 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 6.168 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg 200 4.953 ms - 270893
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 2.447 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 1.672 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg 200 4.397 ms - 270893
[MIXED PLAYLIST] Starting playlist creation process for creator: 91416peucsefexd7z3q9875hw, partner: 31at7552nbs34vogjfzmasypv6vy
[MIXED PLAYLIST] Request parameters: {
name: undefined,
description: 'An AI-blended mix with fresh recommendations',
vibe: undefined,
genres: [ 'pop' ],
includeKnown: true,
createForBoth: false,
limit: 25
}
[MIXED PLAYLIST] Collecting seed tracks from creator (91416peucsefexd7z3q9875hw) and partner (31at7552nbs34vogjfzmasypv6vy)
[MIXED PLAYLIST] Found 20 tracks from creator, 16 tracks from partner
[MIXED PLAYLIST] Total seed track URIs: 36
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.961 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 6.393 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg 200 9.415 ms - 270893
[MIXED PLAYLIST] Creating Spotify playlist for creator: "Vibrant Fusion - Oct 16"
[PLAYLIST CREATION] Creating Spotify playlist: "Vibrant Fusion - Oct 16" for user: 91416peucsefexd7z3q9875hw
[PLAYLIST CREATION] Spotify playlist created successfully with ID: 0b7yrULu6aaBvXpeEUA4nR
[PLAYLIST COVER] Looking for images in directory: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers
[PLAYLIST COVER] Found 9 image files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg'
]
[PLAYLIST COVER] Selected random playlist cover: /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg (from file: WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg)
[PLAYLIST COVER] Looking for file: WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg in files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg'
]
[PLAYLIST COVER] Attempting to upload cover image: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg
[PLAYLIST COVER] Attempting to upload cover image for playlist 0b7yrULu6aaBvXpeEUA4nR from path: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg
[PLAYLIST COVER] Image file size: 100438 bytes (98.08 KB)
[PLAYLIST COVER] Failed to upload cover image for playlist 0b7yrULu6aaBvXpeEUA4nR: { error: { status: 401, message: 'Unauthorized.' } }
[PLAYLIST COVER] Failed to upload cover image to Spotify playlist 0b7yrULu6aaBvXpeEUA4nR, but playlist was created successfully
[MIXED PLAYLIST] Adding 25 tracks to creator playlist 0b7yrULu6aaBvXpeEUA4nR
[MIXED PLAYLIST] Successfully added tracks to creator playlist
[MIXED PLAYLIST] Saving playlist to database: {
id: 'bc5de7f1-f91c-45e8-9549-ff411e05f47d',
name: 'Vibrant Fusion - Oct 16',
trackCount: 25,
creatorPlaylistId: '0b7yrULu6aaBvXpeEUA4nR',
partnerPlaylistId: undefined,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg',
trackUris: [
'spotify:track:5sBDrrtLGbV64QJnEqfjer',
'spotify:track:05od2qm2MTSKCHxy1GBp5W',
'spotify:track:62HoDY1Km6lm47haFpUn9c'
]
}
[MIXED PLAYLIST] Playlist creation completed successfully: {
id: 'bc5de7f1-f91c-45e8-9549-ff411e05f47d',
name: 'Vibrant Fusion - Oct 16',
creatorPlaylistId: '0b7yrULu6aaBvXpeEUA4nR',
partnerPlaylistId: undefined,
trackCount: 25,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg'
}
POST /playlists/mixed 200 1604.637 ms - 1402
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 2.817 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 4.009 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg 200 3.588 ms - 270893
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg 200 6.224 ms - 133957
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 1.734 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.503 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg 200 3.109 ms - 270893
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg 200 2.680 ms - 133957
GET /partners/events/91416peucsefexd7z3q9875hw?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI5MTQxNnBldWNzZWZleGQ3ejNxOTg3NWh3IiwiaWF0IjoxNzYwNjE2MDA3LCJleHAiOjE3NjMyMDgwMDd9.E5NRRqWPxfauImmWE0zi1eQlckg6cPN1i-ojwyXnYuI 200 1.312 ms - -
GET /partners/events/91416peucsefexd7z3q9875hw?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI5MTQxNnBldWNzZWZleGQ3ejNxOTg3NWh3IiwiaWF0IjoxNzYwNjE2MDA3LCJleHAiOjE3NjMyMDgwMDd9.E5NRRqWPxfauImmWE0zi1eQlckg6cPN1i-ojwyXnYuI 200 2.337 ms - -
GET /partners/events/91416peucsefexd7z3q9875hw?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI5MTQxNnBldWNzZWZleGQ3ejNxOTg3NWh3IiwiaWF0IjoxNzYwNjE2MDA3LCJleHAiOjE3NjMyMDgwMDd9.E5NRRqWPxfauImmWE0zi1eQlckg6cPN1i-ojwyXnYuI 200 1.647 ms - -
GET /health 200 0.661 ms - 11
GET /partners/partner/91416peucsefexd7z3q9875hw 200 1.623 ms - 44
GET /playlists/mixed 200 2.029 ms - 6709
GET /partners/partner/91416peucsefexd7z3q9875hw 200 1.555 ms - 44
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 5.543 ms - 156666
GET /partners/requests/91416peucsefexd7z3q9875hw 200 1.618 ms - 2
GET /users/91416peucsefexd7z3q9875hw/status 200 0.735 ms - 57
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 4.746 ms - 156852
GET /users/31at7552nbs34vogjfzmasypv6vy 200 0.913 ms - 238
GET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term 200 2.492 ms - 61623
GET /users/31at7552nbs34vogjfzmasypv6vy 200 2.921 ms - 238
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 5.500 ms - 156852
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 34.610 ms - 20
GET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term 200 3.567 ms - 61623
GET /users/91416peucsefexd7z3q9875hw/status 200 0.847 ms - 57
GET /users/91416peucsefexd7z3q9875hw/recently-played 200 6.017 ms - 156666
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 32.753 ms - 20
GET /users/31at7552nbs34vogjfzmasypv6vy/now-playing 200 2.014 ms - 20
GET /users/31at7552nbs34vogjfzmasypv6vy/recently-played 200 5.002 ms - 156852
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg 200 3.188 ms - 133957
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg 200 6.106 ms - 270893
GET /playlists/mixed 200 2.215 ms - 6709
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 4.627 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 1.706 ms - 131729
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg 200 3.480 ms - 133957
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg 200 3.911 ms - 270893
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 5.660 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.253 ms - 131729
DELETE /playlists/mixed/bc5de7f1-f91c-45e8-9549-ff411e05f47d 200 1.912 ms - 16
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg 200 4.191 ms - 270893
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 3.256 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 4.664 ms - 131729
DELETE /playlists/mixed/8bba02a1-389e-46e8-b4a2-efbb016647a3 200 1.718 ms - 16
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg 200 3.657 ms - 132357
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 5.127 ms - 131729
DELETE /playlists/mixed/f66b57ab-0baa-4740-9303-fedbc5b90df6 200 1.900 ms - 16
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg 200 2.258 ms - 131729
DELETE /playlists/mixed/8791c148-a234-4e08-8ff2-04f54f4348a1 200 2.063 ms - 16
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 88.599 ms - 20
[MIXED PLAYLIST] Starting playlist creation process for creator: 91416peucsefexd7z3q9875hw, partner: 31at7552nbs34vogjfzmasypv6vy
[MIXED PLAYLIST] Request parameters: {
name: undefined,
description: 'An AI-blended mix with fresh recommendations',
vibe: undefined,
genres: undefined,
includeKnown: true,
createForBoth: false,
limit: 25
}
[MIXED PLAYLIST] Collecting seed tracks from creator (91416peucsefexd7z3q9875hw) and partner (31at7552nbs34vogjfzmasypv6vy)
[MIXED PLAYLIST] Found 20 tracks from creator, 16 tracks from partner
[MIXED PLAYLIST] Total seed track URIs: 36
Using fallback genres: [ 'pop', 'rock', 'indie', 'alternative' ] based on track analysis
[MIXED PLAYLIST] Creating Spotify playlist for creator: "Rhythmic Tune - Oct 16"
[PLAYLIST CREATION] Creating Spotify playlist: "Rhythmic Tune - Oct 16" for user: 91416peucsefexd7z3q9875hw
[PLAYLIST CREATION] Spotify playlist created successfully with ID: 3soGn0w8lRVvJrg26j4MFp
[PLAYLIST COVER] Looking for images in directory: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers
[PLAYLIST COVER] Found 9 image files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg'
]
[PLAYLIST COVER] Selected random playlist cover: /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg (from file: WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg)
[PLAYLIST COVER] Looking for file: WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg in files: [
'WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (3).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (4).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29 (5).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.29.jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (1).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30 (2).jpeg',
'WhatsApp Image 2025-10-16 at 11.12.30.jpeg'
]
[PLAYLIST COVER] Attempting to upload cover image: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg
[PLAYLIST COVER] Attempting to upload cover image for playlist 3soGn0w8lRVvJrg26j4MFp from path: /home/pschneid/Gulfslab/projects/spotify/server/public/playlist-covers/WhatsApp Image 2025-10-16 at 11.12.29 (1).jpeg
[PLAYLIST COVER] Image file size: 100438 bytes (98.08 KB)
[PLAYLIST COVER] Failed to upload cover image for playlist 3soGn0w8lRVvJrg26j4MFp: { error: { status: 401, message: 'Unauthorized.' } }
[PLAYLIST COVER] Failed to upload cover image to Spotify playlist 3soGn0w8lRVvJrg26j4MFp, but playlist was created successfully
[MIXED PLAYLIST] Adding 25 tracks to creator playlist 3soGn0w8lRVvJrg26j4MFp
[MIXED PLAYLIST] Successfully added tracks to creator playlist
[MIXED PLAYLIST] Saving playlist to database: {
id: '88d544c9-b1d8-4ec5-9a4e-268430dff2e7',
name: 'Rhythmic Tune - Oct 16',
trackCount: 25,
creatorPlaylistId: '3soGn0w8lRVvJrg26j4MFp',
partnerPlaylistId: undefined,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg',
trackUris: [
'spotify:track:0TFTAtCYhp2tQ9KcJIZb55',
'spotify:track:3DwQ7AH3xGD9h65ezslm6q',
'spotify:track:6S9q9mEifNdnTNlli2xSuD'
]
}
[MIXED PLAYLIST] Playlist creation completed successfully: {
id: '88d544c9-b1d8-4ec5-9a4e-268430dff2e7',
name: 'Rhythmic Tune - Oct 16',
creatorPlaylistId: '3soGn0w8lRVvJrg26j4MFp',
partnerPlaylistId: undefined,
trackCount: 25,
imageUrl: '/api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg'
}
POST /playlists/mixed 200 2897.898 ms - 1401
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg 200 2.457 ms - 133957
GET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg 200 2.064 ms - 133957
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 44.537 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 85.326 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 83.071 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 63.326 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 83.462 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 48.190 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 86.442 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 55.034 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 74.207 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 88.228 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 34.562 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 89.815 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 51.537 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 52.542 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 59.649 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 50.169 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 87.490 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 85.943 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 55.879 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 56.865 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 54.869 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 85.565 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 53.059 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 54.127 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 98.826 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 87.836 ms - 20
GET /users/91416peucsefexd7z3q9875hw/now-playing 200 91.096 ms - 20

1
server-dev.pid Normal file
View File

@ -0,0 +1 @@
619472

26
server.log Normal file
View File

@ -0,0 +1,26 @@
🚀 Production HTTPS Server running on https://159.195.9.107:3443
📁 Serving files from: ./dist/
🔒 SSL Certificate: localhost+2.pem
📥 GET /
📥 GET /assets/index-D8JnZP7b.js
📥 GET /assets/index-F-NF_AMv.css
📥 GET /heart.svg
📥 GET /api/snapshot/91416peucsefexd7z3q9875hw
📥 GET /
📥 GET /assets/index-D8JnZP7b.js
📥 GET /assets/index-F-NF_AMv.css
📥 GET /api/snapshot/91416peucsefexd7z3q9875hw
📥 GET /heart.svg
📥 GET /
📥 GET /assets/index-D8JnZP7b.js
📥 GET /assets/index-F-NF_AMv.css
📥 GET /api/snapshot/31at7552nbs34vogjfzmasypv6vy
📥 GET /heart.svg
📥 GET /heart.svg
📥 GET /
📥 GET /assets/index-D8JnZP7b.js
📥 GET /assets/index-F-NF_AMv.css
📥 GET /api/snapshot/91416peucsefexd7z3q9875hw
📥 GET /
📥 GET /api/snapshot/91416peucsefexd7z3q9875hw
📥 GET /heart.svg

145
server/dist/index.js vendored Normal file
View File

@ -0,0 +1,145 @@
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import fs from 'fs';
import path from 'path';
import https from 'https';
import { db } from './lib/db.js';
import { authRouter } from './routes/auth.js';
import { usersRouter } from './routes/users.js';
import { partnersRouter } from './routes/partners.js';
import { playlistsRouter } from './routes/playlists.js';
import { syncUserData, captureNowPlaying } from './lib/sync.js';
const app = express();
app.use(helmet());
app.use(cors({ origin: [
'http://localhost:4000',
'https://159.195.9.107:3443',
'http://159.195.9.107:3443'
], credentials: true }));
// Add CORS headers to allow mixed content for development
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
next();
});
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: false }));
app.use(morgan('dev'));
// Serve static files from public directory
app.use(express.static('public'));
// Initialize DB
db.initialize();
app.get('/health', (_req, res) => {
res.json({ ok: true });
});
// Placeholder playlist cover endpoint
app.get('/api/placeholder-playlist-cover', (_req, res) => {
// Return a simple SVG placeholder
const svg = `
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8B5CF6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#EC4899;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="300" fill="url(#grad1)"/>
<text x="150" y="150" font-family="Arial, sans-serif" font-size="24" fill="white" text-anchor="middle" dominant-baseline="middle">🎵</text>
</svg>
`;
res.setHeader('Content-Type', 'image/svg+xml');
res.send(svg);
});
// Serve playlist cover images as base64 data URLs to avoid mixed content issues
app.get('/api/playlist-covers/:filename', (req, res) => {
try {
const filename = decodeURIComponent(req.params.filename);
const filePath = path.join(process.cwd(), 'public', 'playlist-covers', filename);
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'Image not found' });
}
// Read file as base64
const fileBuffer = fs.readFileSync(filePath);
const base64Data = fileBuffer.toString('base64');
// Set appropriate content type based on file extension
const ext = path.extname(filename).toLowerCase();
let contentType = 'image/jpeg'; // default
switch (ext) {
case '.png':
contentType = 'image/png';
break;
case '.gif':
contentType = 'image/gif';
break;
case '.webp':
contentType = 'image/webp';
break;
case '.svg':
contentType = 'image/svg+xml';
break;
case '.jpg':
case '.jpeg':
contentType = 'image/jpeg';
break;
}
// Return base64 data URL
const dataUrl = `data:${contentType};base64,${base64Data}`;
res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'public, max-age=31536000'); // Cache for 1 year
res.json({ dataUrl });
}
catch (error) {
console.error('Error serving playlist cover:', error);
res.status(500).json({ error: 'Failed to serve image' });
}
});
app.use('/auth', authRouter);
app.use('/users', usersRouter);
app.use('/partners', partnersRouter);
app.use('/playlists', playlistsRouter);
const PORT = process.env.PORT ? Number(process.env.PORT) : 8081;
const HTTPS_PORT = process.env.HTTPS_PORT ? Number(process.env.HTTPS_PORT) : 8082;
// Start HTTP server
app.listen(PORT, () => {
console.log(`API server (HTTP) listening on http://0.0.0.0:${PORT}`);
});
// Start HTTPS server
try {
const httpsOptions = {
key: fs.readFileSync(path.join(process.cwd(), 'key.pem')),
cert: fs.readFileSync(path.join(process.cwd(), 'cert.pem'))
};
https.createServer(httpsOptions, app).listen(HTTPS_PORT, () => {
console.log(`API server (HTTPS) listening on https://0.0.0.0:${HTTPS_PORT}`);
});
}
catch (error) {
console.log('HTTPS server not started - SSL certificates not found');
}
// Periodic refreshes
const REFRESH_INTERVAL_MS = 2 * 60 * 1000; // full sync
const NOWPLAYING_INTERVAL_MS = 20 * 1000; // capture now playing
setInterval(() => {
try {
const users = db.db.prepare('SELECT id FROM users').all();
for (const u of users) {
syncUserData(u.id).catch(() => void 0);
}
}
catch (e) {
// ignore timer errors
}
}, REFRESH_INTERVAL_MS);
setInterval(() => {
try {
const users = db.db.prepare('SELECT id FROM users').all();
for (const u of users)
captureNowPlaying(u.id).catch(() => void 0);
}
catch { }
}, NOWPLAYING_INTERVAL_MS);

127
server/dist/lib/db.js vendored Normal file
View File

@ -0,0 +1,127 @@
import Database from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
const DB_PATH = process.env.DB_PATH || path.resolve(process.cwd(), '..', 'spotify.db');
class DBWrapper {
constructor() {
this.instance = null;
}
get db() {
if (!this.instance) {
this.instance = new Database(DB_PATH);
this.instance.pragma('journal_mode = WAL');
this.instance.pragma('foreign_keys = ON');
}
return this.instance;
}
initialize() {
// Ensure file exists
if (!fs.existsSync(DB_PATH)) {
fs.writeFileSync(DB_PATH, '');
}
const sql = `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
display_name TEXT,
email TEXT,
avatar_url TEXT,
country TEXT,
product TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
access_token TEXT,
refresh_token TEXT,
token_expires_at INTEGER,
last_synced_at INTEGER
);
CREATE TABLE IF NOT EXISTS friendships (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_a_id TEXT NOT NULL,
user_b_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
UNIQUE(user_a_id, user_b_id)
);
CREATE TABLE IF NOT EXISTS friend_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_user_id TEXT NOT NULL,
to_user_id TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('pending','accepted','declined')),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(from_user_id, to_user_id)
);
CREATE TABLE IF NOT EXISTS recently_played (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
played_at INTEGER NOT NULL,
track_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS top_tracks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
time_range TEXT NOT NULL,
rank INTEGER NOT NULL,
track_json TEXT NOT NULL,
UNIQUE(user_id, time_range, rank)
);
CREATE TABLE IF NOT EXISTS top_artists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
time_range TEXT NOT NULL,
rank INTEGER NOT NULL,
artist_json TEXT NOT NULL,
UNIQUE(user_id, time_range, rank)
);
CREATE TABLE IF NOT EXISTS mixed_playlists (
id TEXT PRIMARY KEY,
creator_id TEXT NOT NULL,
partner_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
vibe TEXT,
genres TEXT, -- JSON array of genres
track_uris TEXT, -- JSON array of track URIs
creator_spotify_id TEXT,
partner_spotify_id TEXT,
creator_spotify_url TEXT,
partner_spotify_url TEXT,
creator_spotify_image_url TEXT,
partner_spotify_image_url TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (creator_id) REFERENCES users(id),
FOREIGN KEY (partner_id) REFERENCES users(id)
);
`;
this.db.exec(sql);
// Migrations for existing DBs
try {
const cols = this.db.prepare("PRAGMA table_info(users)").all();
const hasLastSynced = cols.some(c => c.name === 'last_synced_at');
if (!hasLastSynced) {
this.db.exec('ALTER TABLE users ADD COLUMN last_synced_at INTEGER');
}
}
catch { }
// Migration for mixed_playlists table
try {
const playlistCols = this.db.prepare("PRAGMA table_info(mixed_playlists)").all();
const hasCreatorImageUrl = playlistCols.some(c => c.name === 'creator_spotify_image_url');
const hasPartnerImageUrl = playlistCols.some(c => c.name === 'partner_spotify_image_url');
if (!hasCreatorImageUrl) {
this.db.exec('ALTER TABLE mixed_playlists ADD COLUMN creator_spotify_image_url TEXT');
}
if (!hasPartnerImageUrl) {
this.db.exec('ALTER TABLE mixed_playlists ADD COLUMN partner_spotify_image_url TEXT');
}
}
catch { }
}
}
export const db = new DBWrapper();

48
server/dist/lib/events.js vendored Normal file
View File

@ -0,0 +1,48 @@
const clientsByUser = new Map();
export function addSseClient(userId, res) {
const client = {
userId,
res,
write: (payload) => {
try {
res.write(`data: ${JSON.stringify(payload)}\n\n`);
}
catch { }
},
};
if (!clientsByUser.has(userId))
clientsByUser.set(userId, new Set());
clientsByUser.get(userId).add(client);
// Initial hello
client.write({ type: 'hello', at: Date.now() });
// Heartbeat to keep connections alive through proxies
const heartbeat = setInterval(() => {
try {
res.write(`: ping\n\n`);
}
catch { }
}, 25000);
// Return cleanup
return () => {
clearInterval(heartbeat);
try {
res.end();
}
catch { }
clientsByUser.get(userId)?.delete(client);
if (clientsByUser.get(userId)?.size === 0)
clientsByUser.delete(userId);
};
}
export function sendEvent(userId, event) {
const set = clientsByUser.get(userId);
if (!set || set.size === 0)
return;
for (const client of set)
client.write(event);
}
export function sendToBoth(userA, userB, event) {
sendEvent(userA, event);
if (userB !== userA)
sendEvent(userB, event);
}

359
server/dist/lib/spotify.js vendored Normal file
View File

@ -0,0 +1,359 @@
import axios from 'axios';
import { db } from './db.js';
import fs from 'fs';
import path from 'path';
// Generate a random local playlist cover image URL
function generateRandomPlaylistImage() {
try {
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
console.log(`[PLAYLIST COVER] Looking for images in directory: ${coversDir}`);
// Check if directory exists
if (!fs.existsSync(coversDir)) {
console.log('[PLAYLIST COVER] Directory not found, creating it...');
fs.mkdirSync(coversDir, { recursive: true });
}
// Get all image files from the directory (Spotify only accepts JPEG for cover uploads)
const files = fs.readdirSync(coversDir).filter(file => {
const ext = path.extname(file).toLowerCase();
return ['.jpg', '.jpeg'].includes(ext); // Only JPEG files for Spotify upload
});
console.log(`[PLAYLIST COVER] Found ${files.length} image files:`, files);
// Log file sizes for debugging
files.forEach(file => {
const filePath = path.join(coversDir, file);
const stats = fs.statSync(filePath);
const sizeKB = (stats.size / 1024).toFixed(2);
console.log(`[PLAYLIST COVER] File: ${file}, Size: ${sizeKB} KB`);
});
if (files.length === 0) {
console.log('[PLAYLIST COVER] No playlist cover images found in public/playlist-covers/');
return '/api/placeholder-playlist-cover';
}
// Select a random image
const randomFile = files[Math.floor(Math.random() * files.length)];
const imageUrl = `/api/playlist-covers/${encodeURIComponent(randomFile)}`;
console.log(`[PLAYLIST COVER] Selected random playlist cover: ${imageUrl} (from file: ${randomFile})`);
return imageUrl;
}
catch (error) {
console.error('[PLAYLIST COVER] Error generating random playlist image:', error);
return '/api/placeholder-playlist-cover';
}
}
export function getUserTokens(uid) {
const row = db.db
.prepare('SELECT access_token, refresh_token, token_expires_at FROM users WHERE id = ?')
.get(uid);
if (!row)
return null;
return {
access_token: row.access_token,
refresh_token: row.refresh_token,
token_expires_at: row.token_expires_at,
};
}
export async function ensureValidAccessToken(uid) {
const tokens = getUserTokens(uid);
if (!tokens?.access_token) {
throw new Error('User not authenticated');
}
const now = Date.now();
if (tokens.token_expires_at && tokens.token_expires_at > now + 30000) {
return tokens.access_token;
}
if (!tokens.refresh_token) {
return tokens.access_token;
}
// Refresh token via Spotify
const clientId = process.env.SPOTIFY_CLIENT_ID || '';
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET || '';
const resp = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({ grant_type: 'refresh_token', refresh_token: tokens.refresh_token }), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
},
});
const { access_token, expires_in } = resp.data;
const token_expires_at = Date.now() + expires_in * 1000 - 60000;
db.db
.prepare('UPDATE users SET access_token = ?, token_expires_at = ?, updated_at=? WHERE id = ?')
.run(access_token, token_expires_at, Date.now(), uid);
return access_token;
}
export function vibeToTargets(vibe) {
switch (vibe) {
case 'energetic':
return { target_energy: 0.85, target_danceability: 0.7, target_tempo: 130 };
case 'chill':
return { target_energy: 0.3, target_danceability: 0.4, target_tempo: 90 };
case 'happy':
return { target_valence: 0.85, target_danceability: 0.6 };
case 'sad':
return { target_valence: 0.2 };
case 'party':
return { target_danceability: 0.85, target_energy: 0.8, target_tempo: 125 };
case 'focus':
return { target_energy: 0.4, target_instrumentalness: 0.5 };
default:
return {};
}
}
export async function fetchRecommendations(accessToken, seeds, targets, limit = 25) {
// Legacy kept for compatibility; prefer fetchSimilarTrackUris below
return fetchSimilarTrackUris(accessToken, seeds.seed_tracks, seeds.seed_artists, seeds.seed_genres, targets, limit);
}
export async function createSpotifyPlaylist(accessToken, userId, name, description) {
console.log(`[PLAYLIST CREATION] Creating Spotify playlist: "${name}" for user: ${userId}`);
const resp = await axios.post(`https://api.spotify.com/v1/users/${encodeURIComponent(userId)}/playlists`, { name, description, public: false }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } });
const pl = resp.data;
console.log(`[PLAYLIST CREATION] Spotify playlist created successfully with ID: ${pl.id}`);
// Generate a random local playlist cover image
const imageUrl = generateRandomPlaylistImage();
// Extract the filename from the imageUrl to use the SAME image for Spotify upload
const selectedFilename = decodeURIComponent(imageUrl.replace('/api/playlist-covers/', ''));
console.log(`[PLAYLIST COVER] Selected image for both website and Spotify: ${selectedFilename}`);
// Get the actual file path for uploading to Spotify (only JPEG files)
const coversDir = path.join(process.cwd(), 'public', 'playlist-covers');
const files = fs.readdirSync(coversDir).filter(file => {
const ext = path.extname(file).toLowerCase();
return ['.jpg', '.jpeg'].includes(ext); // Only JPEG files for Spotify upload
});
if (files.length > 0) {
// Find the specific file that was selected for the website
const selectedFile = files.find(file => file === selectedFilename);
if (selectedFile) {
const imagePath = path.join(coversDir, selectedFile);
const stats = fs.statSync(imagePath);
const fileSizeKB = (stats.size / 1024).toFixed(2);
console.log(`[PLAYLIST COVER] Using SAME image for Spotify upload: ${selectedFile} (${fileSizeKB} KB)`);
// Upload the SAME cover image to Spotify
const uploadSuccess = await uploadPlaylistCover(accessToken, pl.id, imagePath);
if (uploadSuccess) {
console.log(`[PLAYLIST COVER] ✅ Successfully uploaded SAME cover image to Spotify playlist ${pl.id}`);
}
else {
console.log(`[PLAYLIST COVER] ❌ Failed to upload ${selectedFile} to Spotify, but website will still show it`);
}
}
else {
console.log(`[PLAYLIST COVER] ⚠️ Could not find selected file ${selectedFilename} for Spotify upload`);
console.log(`[PLAYLIST COVER] ✅ WEBSITE COVER: Random cover image will still be displayed on your website.`);
}
}
return { id: pl.id, url: pl.external_urls?.spotify, imageUrl };
}
export async function addTracks(accessToken, playlistId, trackUris) {
if (!trackUris.length)
return;
await axios.post(`https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/tracks`, { uris: trackUris }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } });
}
// Upload a playlist cover image to Spotify
export async function uploadPlaylistCover(accessToken, playlistId, imagePath) {
try {
console.log(`[PLAYLIST COVER] Attempting to upload cover image for playlist ${playlistId} from path: ${imagePath}`);
// Check if file exists
if (!fs.existsSync(imagePath)) {
console.error(`[PLAYLIST COVER] Image file not found: ${imagePath}`);
return false;
}
// Read the image file
let imageBuffer = fs.readFileSync(imagePath);
let fileSize = imageBuffer.length;
console.log(`[PLAYLIST COVER] Original image file size: ${fileSize} bytes (${(fileSize / 1024).toFixed(2)} KB)`);
// Check if we need to compress the image
// We need to account for base64 encoding overhead (33% increase)
// So we want the original file to be under ~190 KB to stay under 256 KB when base64 encoded
const maxOriginalSize = 190 * 1024; // 190 KB
if (fileSize > maxOriginalSize) {
console.log(`[PLAYLIST COVER] Image too large (${(fileSize / 1024).toFixed(2)} KB), attempting to compress...`);
// For now, let's skip files that are too large and try a different one
console.error(`[PLAYLIST COVER] Image file too large for Spotify upload: ${fileSize} bytes (max ~190 KB to account for base64 overhead)`);
return false;
}
// Convert to base64
let base64Image = imageBuffer.toString('base64');
const base64Size = base64Image.length;
console.log(`[PLAYLIST COVER] Base64 length: ${base64Size} characters (${(base64Size / 1024).toFixed(2)} KB)`);
// Ensure we don't have any data URL prefix
if (base64Image.startsWith('data:')) {
base64Image = base64Image.split(',')[1] || base64Image;
console.log(`[PLAYLIST COVER] Removed data URL prefix from base64`);
}
// Final check - ensure base64 size is reasonable
if (base64Size > 300 * 1024) { // 300 KB base64 limit
console.error(`[PLAYLIST COVER] Base64 encoded image too large: ${base64Size} characters`);
return false;
}
// Determine content type based on file extension
const ext = path.extname(imagePath).toLowerCase();
let contentType = 'image/jpeg'; // default
switch (ext) {
case '.png':
contentType = 'image/png';
break;
case '.gif':
contentType = 'image/gif';
break;
case '.webp':
contentType = 'image/webp';
break;
case '.svg':
contentType = 'image/svg+xml';
break;
case '.jpg':
case '.jpeg':
contentType = 'image/jpeg';
break;
}
console.log(`[PLAYLIST COVER] Using content type: ${contentType}`);
// Check token scopes first
try {
console.log(`[PLAYLIST COVER] Checking token scopes...`);
const meResponse = await axios.get('https://api.spotify.com/v1/me', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
console.log(`[PLAYLIST COVER] Token is valid for user: ${meResponse.data.display_name}`);
// Test if we can access the playlist
const playlistResponse = await axios.get(`https://api.spotify.com/v1/playlists/${playlistId}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
console.log(`[PLAYLIST COVER] Can access playlist: ${playlistResponse.data.name}`);
}
catch (error) {
console.error(`[PLAYLIST COVER] Token validation failed:`, error.response?.data || error.message);
}
// Upload to Spotify
console.log(`[PLAYLIST COVER] Making request to Spotify API: https://api.spotify.com/v1/playlists/${playlistId}/images`);
console.log(`[PLAYLIST COVER] Access token (first 20 chars): ${accessToken.substring(0, 20)}...`);
// Try different approaches for image upload
let response;
try {
// Method 1: Standard PUT request (requires ugc-image-upload scope)
response = await axios.put(`https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/images`, base64Image, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': contentType
}
});
}
catch (error) {
if (error.response?.status === 401) {
console.log(`[PLAYLIST COVER] Method 1 failed with 401, trying alternative approach...`);
// Method 2: Try with different content type
try {
response = await axios.put(`https://api.spotify.com/v1/playlists/${encodeURIComponent(playlistId)}/images`, base64Image, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'image/jpeg' // Force JPEG content type
}
});
}
catch (error2) {
console.log(`[PLAYLIST COVER] Method 2 also failed: ${error2.response?.status}`);
throw error; // Re-throw original error
}
}
else {
throw error; // Re-throw if not 401
}
}
console.log(`[PLAYLIST COVER] Successfully uploaded cover image for playlist ${playlistId}, response status: ${response.status}`);
console.log(`[PLAYLIST COVER] Response headers:`, response.headers);
return true;
}
catch (error) {
console.error(`[PLAYLIST COVER] Failed to upload cover image for playlist ${playlistId}:`);
console.error(`[PLAYLIST COVER] Error status:`, error.response?.status);
console.error(`[PLAYLIST COVER] Error data:`, error.response?.data);
console.error(`[PLAYLIST COVER] Error message:`, error.message);
return false;
}
}
// ----- New: Similar tracks without recommendations endpoint -----
async function getRelatedArtists(accessToken, artistId) {
const resp = await axios.get(`https://api.spotify.com/v1/artists/${encodeURIComponent(artistId)}/related-artists`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const artists = (resp.data?.artists || []);
return artists.map(a => a.id).filter(Boolean);
}
async function getArtistTopTracks(accessToken, artistId) {
const resp = await axios.get(`https://api.spotify.com/v1/artists/${encodeURIComponent(artistId)}/top-tracks?market=from_token`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const tracks = (resp.data?.tracks || []);
return tracks.map(t => t.uri || (t.id ? `spotify:track:${t.id}` : null)).filter((u) => !!u);
}
async function searchTracksByGenre(accessToken, genre, limit) {
const resp = await axios.get('https://api.spotify.com/v1/search', {
headers: { Authorization: `Bearer ${accessToken}` },
params: { q: `genre:"${genre}"`, type: 'track', limit: Math.min(limit, 50) },
});
const tracks = (resp.data?.tracks?.items || []);
return tracks.map(t => t.uri || (t.id ? `spotify:track:${t.id}` : null)).filter((u) => !!u);
}
async function filterByTargets(accessToken, trackUris, targets) {
if (!trackUris.length || Object.keys(targets).length === 0)
return trackUris;
const ids = trackUris.map(u => u.split(':').pop()).filter(Boolean);
const resp = await axios.get('https://api.spotify.com/v1/audio-features', {
headers: { Authorization: `Bearer ${accessToken}` },
params: { ids: ids.slice(0, 100).join(',') },
});
const features = (resp.data?.audio_features || []);
const pass = new Set(features
.filter(f => !!f && (!('target_energy' in targets) || Math.abs(targets.target_energy - f.energy) <= 0.4))
.filter(f => !('target_danceability' in targets) || Math.abs(targets.target_danceability - f.danceability) <= 0.4)
.filter(f => !('target_valence' in targets) || Math.abs(targets.target_valence - f.valence) <= 0.5)
.filter(f => !('target_tempo' in targets) || Math.abs(targets.target_tempo - f.tempo) <= 30)
.filter(f => !('target_instrumentalness' in targets) || Math.abs(targets.target_instrumentalness - f.instrumentalness) <= 0.5)
.map(f => f.id));
return trackUris.filter(u => pass.has(u.split(':').pop()));
}
export async function fetchSimilarTrackUris(accessToken, seedTrackUris, seedArtistIds, seedGenres, targets = {}, limit = 25) {
const candidateUris = new Set();
// From seed artists and from artists of seed tracks
const trackIds = (seedTrackUris || []).map(u => u?.split(':').pop()).filter(Boolean);
const seedArtistSet = new Set(seedArtistIds || []);
// If we got tracks, fetch their artists to expand
if (trackIds.length) {
const tracksResp = await axios.get('https://api.spotify.com/v1/tracks', {
headers: { Authorization: `Bearer ${accessToken}` },
params: { ids: trackIds.slice(0, 50).join(',') },
});
const tracks = (tracksResp.data?.tracks || []);
tracks.forEach(t => (t.artists || []).forEach(a => a?.id && seedArtistSet.add(a.id)));
}
// Expand via related artists and take top tracks
const seedArtists = Array.from(seedArtistSet).slice(0, 10);
for (const artistId of seedArtists) {
try {
const related = (await getRelatedArtists(accessToken, artistId)).slice(0, 10);
const pool = [artistId, ...related];
for (const a of pool) {
const tops = await getArtistTopTracks(accessToken, a);
tops.forEach(u => candidateUris.add(u));
if (candidateUris.size >= limit * 4)
break;
}
if (candidateUris.size >= limit * 4)
break;
}
catch { }
}
// Genre-based search fallback
for (const g of (seedGenres || []).slice(0, 3)) {
try {
const found = await searchTracksByGenre(accessToken, g, 50);
found.forEach(u => candidateUris.add(u));
if (candidateUris.size >= limit * 4)
break;
}
catch { }
}
let uris = Array.from(candidateUris);
uris = await filterByTargets(accessToken, uris, targets);
// Shuffle and take limit
uris.sort(() => Math.random() - 0.5);
return uris.slice(0, limit);
}

82
server/dist/lib/sync.js vendored Normal file
View File

@ -0,0 +1,82 @@
import axios from 'axios';
import { db } from './db.js';
import { ensureValidAccessToken } from './spotify.js';
export async function syncUserData(uid) {
const accessToken = await ensureValidAccessToken(uid);
const headers = { Authorization: `Bearer ${accessToken}` };
const [recentResp, topTracksShort, topTracksMed, topArtistsShort, topArtistsMed] = await Promise.all([
axios.get('https://api.spotify.com/v1/me/player/recently-played?limit=20', { headers }),
axios.get('https://api.spotify.com/v1/me/top/tracks?time_range=short_term&limit=20', { headers }),
axios.get('https://api.spotify.com/v1/me/top/tracks?time_range=medium_term&limit=20', { headers }),
axios.get('https://api.spotify.com/v1/me/top/artists?time_range=short_term&limit=20', { headers }),
axios.get('https://api.spotify.com/v1/me/top/artists?time_range=medium_term&limit=20', { headers })
]);
const trx = db.db.transaction(() => {
const rpStmt = db.db.prepare('INSERT OR REPLACE INTO recently_played (id, user_id, played_at, track_json) VALUES (?, ?, ?, ?)');
for (const item of recentResp.data.items) {
rpStmt.run(item.played_at + ':' + item.track.id, uid, new Date(item.played_at).getTime(), JSON.stringify(item.track));
}
const ttDel = db.db.prepare('DELETE FROM top_tracks WHERE user_id=? AND time_range=?');
const ttIns = db.db.prepare('INSERT INTO top_tracks (user_id, time_range, rank, track_json) VALUES (?, ?, ?, ?)');
for (const [range, payload] of [
['short_term', topTracksShort.data],
['medium_term', topTracksMed.data],
]) {
ttDel.run(uid, range);
payload.items.forEach((t, idx) => ttIns.run(uid, range, idx + 1, JSON.stringify(t)));
}
const taDel = db.db.prepare('DELETE FROM top_artists WHERE user_id=? AND time_range=?');
const taIns = db.db.prepare('INSERT INTO top_artists (user_id, time_range, rank, artist_json) VALUES (?, ?, ?, ?)');
for (const [range, payload] of [
['short_term', topArtistsShort.data],
['medium_term', topArtistsMed.data],
]) {
taDel.run(uid, range);
payload.items.forEach((a, idx) => taIns.run(uid, range, idx + 1, JSON.stringify(a)));
}
});
trx();
db.db.prepare('UPDATE users SET last_synced_at=?, updated_at=? WHERE id=?').run(Date.now(), Date.now(), uid);
}
// Capture the user's current playback and persist as a recent play if changed
export async function captureNowPlaying(uid) {
try {
const accessToken = await ensureValidAccessToken(uid);
const headers = { Authorization: `Bearer ${accessToken}` };
const resp = await axios.get('https://api.spotify.com/v1/me/player/currently-playing?additional_types=track', {
headers,
validateStatus: () => true,
});
if (resp.status !== 200)
return; // nothing to record
const data = resp.data;
if (!data?.is_playing || !data?.item)
return;
const track = data.item;
const trackId = track?.id;
if (!trackId)
return;
// Deduplicate: update existing row if same track found recently
const recentRows = db.db.prepare('SELECT id, track_json, played_at FROM recently_played WHERE user_id=? ORDER BY played_at DESC LIMIT 50').all(uid);
for (const row of recentRows) {
try {
const rowTrack = JSON.parse(row.track_json);
if (rowTrack?.id === trackId) {
// If the last record for this track is within 15 minutes, treat as the same session; update timestamp
if (Date.now() - row.played_at < 15 * 60 * 1000) {
db.db.prepare('UPDATE recently_played SET played_at = ?, track_json = ? WHERE id = ?').run(Date.now(), JSON.stringify(track), row.id);
return;
}
break;
}
}
catch { }
}
db.db
.prepare('INSERT INTO recently_played (user_id, played_at, track_json) VALUES (?, ?, ?)')
.run(uid, Date.now(), JSON.stringify(track));
}
catch {
// ignore errors while capturing now playing to keep background loop robust
}
}

19
server/dist/middleware/auth.js vendored Normal file
View File

@ -0,0 +1,19 @@
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change';
export function requireAuth(req, res, next) {
const header = req.headers.authorization || '';
let token = header.startsWith('Bearer ') ? header.slice(7) : null;
// Allow token via query param for SSE EventSource
if (!token && typeof req.query?.token === 'string')
token = String(req.query.token);
if (!token)
return res.status(401).json({ error: 'Missing token' });
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = { uid: payload.uid };
next();
}
catch {
return res.status(401).json({ error: 'Invalid token' });
}
}

91
server/dist/routes/auth.js vendored Normal file
View File

@ -0,0 +1,91 @@
import { Router } from 'express';
import axios from 'axios';
import jwt from 'jsonwebtoken';
import { db } from '../lib/db.js';
export const authRouter = Router();
const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID || '';
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET || '';
const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/callback';
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change';
authRouter.post('/exchange', async (req, res) => {
try {
const { code } = req.body;
if (!code)
return res.status(400).json({ error: 'Missing code' });
const tokenResp = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: REDIRECT_URI,
}), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`,
},
});
const { access_token, refresh_token, expires_in } = tokenResp.data;
const meResp = await axios.get('https://api.spotify.com/v1/me', {
headers: { Authorization: `Bearer ${access_token}` },
});
const profile = meResp.data;
const now = Date.now();
const tokenExpiresAt = now + expires_in * 1000 - 60000; // refresh a bit early
db.db
.prepare(`INSERT INTO users (id, display_name, email, avatar_url, country, product, created_at, updated_at, access_token, refresh_token, token_expires_at)
VALUES (@id, @display_name, @email, @avatar_url, @country, @product, @created_at, @updated_at, @access_token, @refresh_token, @token_expires_at)
ON CONFLICT(id) DO UPDATE SET
display_name=excluded.display_name,
email=excluded.email,
avatar_url=excluded.avatar_url,
country=excluded.country,
product=excluded.product,
updated_at=excluded.updated_at,
access_token=excluded.access_token,
refresh_token=excluded.refresh_token,
token_expires_at=excluded.token_expires_at`)
.run({
id: profile.id,
display_name: profile.display_name,
email: profile.email,
avatar_url: profile.images?.[0]?.url || null,
country: profile.country,
product: profile.product,
created_at: now,
updated_at: now,
access_token,
refresh_token,
token_expires_at: tokenExpiresAt,
});
const jwtToken = jwt.sign({ uid: profile.id }, JWT_SECRET, { expiresIn: '30d' });
// Return Spotify tokens to enable client-side playback controls
res.json({ token: jwtToken, uid: profile.id, access_token, refresh_token, expires_in });
}
catch (err) {
const detail = err?.response?.data || err?.message || 'Unknown error';
console.error('Auth exchange error:', detail);
res.status(500).json({ error: 'Auth exchange failed', detail });
}
});
authRouter.post('/refresh', async (req, res) => {
try {
const { uid } = req.body;
if (!uid)
return res.status(400).json({ error: 'Missing uid' });
const row = db.db.prepare('SELECT refresh_token FROM users WHERE id = ?').get(uid);
if (!row?.refresh_token)
return res.status(404).json({ error: 'User not found' });
const tokenResp = await axios.post('https://accounts.spotify.com/api/token', new URLSearchParams({ grant_type: 'refresh_token', refresh_token: row.refresh_token }), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`,
},
});
const { access_token, expires_in } = tokenResp.data;
const tokenExpiresAt = Date.now() + expires_in * 1000 - 60000;
db.db.prepare('UPDATE users SET access_token=?, token_expires_at=?, updated_at=? WHERE id=?').run(access_token, tokenExpiresAt, Date.now(), uid);
res.json({ ok: true });
}
catch (err) {
console.error(err.response?.data || err.message);
res.status(500).json({ error: 'Refresh failed' });
}
});

172
server/dist/routes/partners.js vendored Normal file
View File

@ -0,0 +1,172 @@
import { Router } from 'express';
import { db } from '../lib/db.js';
import { addSseClient, sendEvent, sendToBoth } from '../lib/events.js';
import { requireAuth } from '../middleware/auth.js';
export const partnersRouter = Router();
// Send a partner request (not via link) by target uid
partnersRouter.post('/request', requireAuth, (req, res) => {
const { toUid } = req.body;
const fromUid = req.user?.uid;
if (!fromUid || !toUid || fromUid === toUid)
return res.status(400).json({ error: 'Invalid uids' });
const now = Date.now();
try {
db.db
.prepare(`INSERT INTO friend_requests (from_user_id, to_user_id, status, created_at, updated_at)
VALUES (?, ?, 'pending', ?, ?)`)
.run(fromUid, toUid, now, now);
// Notify the recipient about a new incoming request
sendEvent(toUid, { type: 'partner:request', fromUserId: fromUid, at: now });
res.json({ ok: true });
}
catch (e) {
if (e && String(e.message).includes('UNIQUE')) {
return res.status(409).json({ error: 'Request already exists' });
}
throw e;
}
});
// List incoming requests for a user
partnersRouter.get('/requests/:uid', requireAuth, (req, res) => {
const { uid } = req.params;
if (!req.user || req.user.uid !== uid)
return res.status(403).json({ error: 'Forbidden' });
const rows = db.db
.prepare("SELECT id, from_user_id, to_user_id, status, created_at FROM friend_requests WHERE to_user_id = ? AND status = 'pending' ORDER BY created_at DESC")
.all(uid);
res.json(rows);
});
// Accept a request
partnersRouter.post('/requests/:id/accept', requireAuth, (req, res) => {
const { id } = req.params;
const reqRow = db.db.prepare('SELECT * FROM friend_requests WHERE id = ?').get(id);
if (!reqRow)
return res.status(404).json({ error: 'Not found' });
if (!req.user || req.user.uid !== reqRow.to_user_id)
return res.status(403).json({ error: 'Forbidden' });
const now = Date.now();
const userA = reqRow.from_user_id < reqRow.to_user_id ? reqRow.from_user_id : reqRow.to_user_id;
const userB = reqRow.from_user_id < reqRow.to_user_id ? reqRow.to_user_id : reqRow.from_user_id;
const txn = db.db.transaction(() => {
db.db.prepare("UPDATE friend_requests SET status='accepted', updated_at=? WHERE id=?").run(now, id);
db.db.prepare('INSERT OR IGNORE INTO friendships (user_a_id, user_b_id, created_at) VALUES (?, ?, ?)').run(userA, userB, now);
});
txn();
// Notify both users of partnership established
sendToBoth(userA, userB, { type: 'partner:connected', a: userA, b: userB, at: now });
res.json({ ok: true });
});
// Decline a request
partnersRouter.post('/requests/:id/decline', requireAuth, (req, res) => {
const { id } = req.params;
const now = Date.now();
const reqRow = db.db.prepare('SELECT * FROM friend_requests WHERE id = ?').get(id);
if (!reqRow)
return res.status(404).json({ error: 'Not found' });
if (!req.user || req.user.uid !== reqRow.to_user_id)
return res.status(403).json({ error: 'Forbidden' });
db.db.prepare("UPDATE friend_requests SET status='declined', updated_at=? WHERE id=?").run(now, id);
// Notify requester that their request was declined
sendEvent(reqRow.from_user_id, { type: 'partner:declined', byUserId: reqRow.to_user_id, requestId: reqRow.id, at: now });
res.json({ ok: true });
});
// Check partnership between two users
partnersRouter.get('/status', (req, res) => {
const { a, b } = req.query;
if (!a || !b)
return res.status(400).json({ error: 'Missing params' });
const [userA, userB] = a < b ? [a, b] : [b, a];
const exists = db.db
.prepare('SELECT 1 FROM friendships WHERE user_a_id=? AND user_b_id=?')
.get(userA, userB);
res.json({ partnered: !!exists });
});
// Fetch partner for a user (if any)
partnersRouter.get('/partner/:uid', requireAuth, (req, res) => {
const { uid } = req.params;
if (!req.user || req.user.uid !== uid)
return res.status(403).json({ error: 'Forbidden' });
const row = db.db
.prepare(`SELECT CASE WHEN user_a_id = ? THEN user_b_id ELSE user_a_id END as partner_id
FROM friendships
WHERE user_a_id = ? OR user_b_id = ?
LIMIT 1`)
.get(uid, uid, uid);
if (!row?.partner_id)
return res.json({ partnerId: null });
return res.json({ partnerId: row.partner_id });
});
// SSE: subscribe to partner events for authed user
partnersRouter.get('/events/:uid', requireAuth, (req, res) => {
const { uid } = req.params;
if (!req.user || req.user.uid !== uid)
return res.status(403).json({ error: 'Forbidden' });
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders?.();
const cleanup = addSseClient(uid, res);
req.on('close', cleanup);
});
// Remove an existing partnership between the authed user and a specific partner
partnersRouter.post('/remove', requireAuth, (req, res) => {
const { partnerId } = req.body;
const uid = req.user?.uid;
if (!uid || !partnerId || uid === partnerId)
return res.status(400).json({ error: 'Invalid partnerId' });
const userA = uid < partnerId ? uid : partnerId;
const userB = uid < partnerId ? partnerId : uid;
const existing = db.db
.prepare('SELECT id FROM friendships WHERE user_a_id=? AND user_b_id=?')
.get(userA, userB);
if (!existing?.id)
return res.status(404).json({ error: 'No active partnership' });
try {
const txn = db.db.transaction(() => {
db.db.prepare('DELETE FROM friendships WHERE user_a_id=? AND user_b_id=?').run(userA, userB);
// Remove any historical or pending requests between the two users so they can re-invite
db.db
.prepare('DELETE FROM friend_requests WHERE (from_user_id=? AND to_user_id=?) OR (from_user_id=? AND to_user_id=?)')
.run(uid, partnerId, partnerId, uid);
});
txn();
// Notify both users of disconnection
sendToBoth(userA, userB, { type: 'partner:disconnected', a: userA, b: userB, at: Date.now() });
return res.json({ ok: true });
}
catch (e) {
return res.status(500).json({ error: 'Failed to remove partnership' });
}
});
// Clear any existing partnership for the authed user (fallback helper)
partnersRouter.post('/clear', requireAuth, (req, res) => {
const { userId } = req.body;
const uid = req.user?.uid;
if (!uid || !userId || uid !== userId)
return res.status(403).json({ error: 'Forbidden' });
try {
// Find all partners (normally only one) and delete links + requests
const partners = db.db
.prepare('SELECT CASE WHEN user_a_id = ? THEN user_b_id ELSE user_a_id END as partner_id FROM friendships WHERE user_a_id = ? OR user_b_id = ?')
.all(uid, uid, uid);
const txn = db.db.transaction(() => {
db.db.prepare('DELETE FROM friendships WHERE user_a_id=? OR user_b_id=?').run(uid, uid);
for (const row of partners) {
db.db
.prepare('DELETE FROM friend_requests WHERE (from_user_id=? AND to_user_id=?) OR (from_user_id=? AND to_user_id=?)')
.run(uid, row.partner_id, row.partner_id, uid);
}
});
txn();
// Notify the user and any former partners
for (const row of partners) {
const a = uid < row.partner_id ? uid : row.partner_id;
const b = uid < row.partner_id ? row.partner_id : uid;
sendToBoth(a, b, { type: 'partner:disconnected', a, b, at: Date.now() });
}
return res.json({ ok: true });
}
catch (e) {
return res.status(500).json({ error: 'Failed to clear partnership' });
}
});

209
server/dist/routes/playlists.js vendored Normal file
View File

@ -0,0 +1,209 @@
import { Router } from 'express';
import { requireAuth } from '../middleware/auth.js';
import { addTracks, createSpotifyPlaylist, ensureValidAccessToken, fetchRecommendations, vibeToTargets } from '../lib/spotify.js';
import { db } from '../lib/db.js';
import { v4 as uuidv4 } from 'uuid';
export const playlistsRouter = Router();
// Helper function to generate unique playlist names
function generateUniquePlaylistName() {
const adjectives = ['Vibrant', 'Melodic', 'Harmonic', 'Rhythmic', 'Ethereal', 'Dynamic', 'Soulful', 'Electric', 'Acoustic', 'Cosmic', 'Urban', 'Chill', 'Energetic', 'Dreamy', 'Funky'];
const nouns = ['Fusion', 'Symphony', 'Vibes', 'Journey', 'Harmony', 'Blend', 'Mix', 'Soundscape', 'Melody', 'Rhythm', 'Groove', 'Beat', 'Tune', 'Sound', 'Wave'];
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const noun = nouns[Math.floor(Math.random() * nouns.length)];
const timestamp = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return `${adjective} ${noun} - ${timestamp}`;
}
// GET /playlists/mixed - Get all mixed playlists for a user
playlistsRouter.get('/mixed', requireAuth, async (req, res) => {
try {
const userId = req.user.uid;
const playlists = db.db.prepare(`
SELECT * FROM mixed_playlists
WHERE creator_id = ? OR partner_id = ?
ORDER BY created_at DESC
`).all(userId, userId);
res.json({ playlists });
}
catch (err) {
console.error('Failed to fetch mixed playlists:', err);
res.status(500).json({ error: 'Failed to fetch playlists' });
}
});
// DELETE /playlists/mixed/:id - Delete a mixed playlist
playlistsRouter.delete('/mixed/:id', requireAuth, async (req, res) => {
try {
const userId = req.user.uid;
const playlistId = req.params.id;
// Check if user owns this playlist
const playlist = db.db.prepare('SELECT * FROM mixed_playlists WHERE id = ? AND (creator_id = ? OR partner_id = ?)').get(playlistId, userId, userId);
if (!playlist) {
return res.status(404).json({ error: 'Playlist not found' });
}
// Delete from database
db.db.prepare('DELETE FROM mixed_playlists WHERE id = ?').run(playlistId);
res.json({ success: true });
}
catch (err) {
console.error('Failed to delete playlist:', err);
res.status(500).json({ error: 'Failed to delete playlist' });
}
});
// POST /playlists/mixed
// body: { name?: string, description?: string, vibe?: string, genres?: string[], includeKnown?: boolean, partnerId?: string, createForBoth?: boolean, limit?: number }
playlistsRouter.post('/mixed', requireAuth, async (req, res) => {
try {
const creatorId = req.user.uid;
const { name, description, vibe, genres, includeKnown = true, partnerId, createForBoth = false, limit = 25 } = req.body || {};
console.log(`[MIXED PLAYLIST] Starting playlist creation process for creator: ${creatorId}, partner: ${partnerId}`);
console.log(`[MIXED PLAYLIST] Request parameters:`, { name, description, vibe, genres, includeKnown, createForBoth, limit });
if (!partnerId)
return res.status(400).json({ error: 'partnerId required' });
// Collect seeds from both users' top tracks
console.log(`[MIXED PLAYLIST] Collecting seed tracks from creator (${creatorId}) and partner (${partnerId})`);
const seedRows1 = db.db.prepare('SELECT track_json FROM top_tracks WHERE user_id=? AND time_range=? ORDER BY rank ASC LIMIT 20').all(creatorId, 'short_term');
const seedRows2 = db.db.prepare('SELECT track_json FROM top_tracks WHERE user_id=? AND time_range=? ORDER BY rank ASC LIMIT 20').all(partnerId, 'short_term');
const tracks1 = seedRows1.map((r) => JSON.parse(r.track_json));
const tracks2 = seedRows2.map((r) => JSON.parse(r.track_json));
console.log(`[MIXED PLAYLIST] Found ${tracks1.length} tracks from creator, ${tracks2.length} tracks from partner`);
let seedTrackUris = [...tracks1, ...tracks2]
.map((t) => t.uri || (t.id ? `spotify:track:${t.id}` : null))
.filter((u) => !!u);
console.log(`[MIXED PLAYLIST] Total seed track URIs: ${seedTrackUris.length}`);
// Fallback to artist seeds if track seeds are empty
let seedArtistIds = [];
if (seedTrackUris.length === 0) {
const artistIds = [...tracks1, ...tracks2]
.flatMap((t) => t.artists || [])
.map((a) => a?.id)
.filter((id) => !!id);
seedArtistIds = Array.from(new Set(artistIds));
}
// Use at most 5 seeds of each type
seedTrackUris = seedTrackUris.slice(0, 5);
seedArtistIds = seedArtistIds.slice(0, 5);
const creatorToken = await ensureValidAccessToken(creatorId);
const targets = vibeToTargets(vibe);
// Always provide fallback genres if none specified
let finalGenres = genres;
if (!Array.isArray(genres) || genres.length === 0) {
// Analyze users' top tracks to determine appropriate genres
const allTracks = [...tracks1, ...tracks2];
const genreCounts = {};
// Count genres from top tracks
allTracks.forEach(track => {
if (track.artists && track.artists.length > 0) {
// Use common genres based on artist analysis
// This is a simplified approach - in a real implementation, you'd analyze track features
genreCounts['pop'] = (genreCounts['pop'] || 0) + 1;
genreCounts['rock'] = (genreCounts['rock'] || 0) + 1;
genreCounts['indie'] = (genreCounts['indie'] || 0) + 1;
genreCounts['alternative'] = (genreCounts['alternative'] || 0) + 1;
}
});
// Use top genres or fallback to popular ones
const sortedGenres = Object.entries(genreCounts)
.sort(([, a], [, b]) => b - a)
.map(([genre]) => genre)
.slice(0, 4);
finalGenres = sortedGenres.length > 0 ? sortedGenres : ['pop', 'rock', 'indie', 'alternative'];
console.log('Using fallback genres:', finalGenres, 'based on track analysis');
}
const recommendedUris = await fetchRecommendations(creatorToken, {
seed_tracks: seedTrackUris.length ? seedTrackUris : undefined,
seed_artists: seedArtistIds.length ? seedArtistIds : undefined,
seed_genres: finalGenres,
}, targets, limit);
// Optionally remove already listened tracks for both users (recently played)
let finalUris = recommendedUris;
if (!includeKnown) {
const recents1 = db.db.prepare('SELECT track_json FROM recently_played WHERE user_id=? ORDER BY played_at DESC LIMIT 100').all(creatorId);
const recents2 = db.db.prepare('SELECT track_json FROM recently_played WHERE user_id=? ORDER BY played_at DESC LIMIT 100').all(partnerId);
const recentUris = new Set([...recents1, ...recents2]
.map((r) => JSON.parse(r.track_json))
.map((t) => t.uri || (t.id ? `spotify:track:${t.id}` : null))
.filter((u) => !!u));
finalUris = recommendedUris.filter((u) => !recentUris.has(u));
}
// Generate unique playlist name if not provided
const playlistName = name || generateUniquePlaylistName();
const playlistDesc = description || 'An AI-blended mix with fresh recommendations';
// Always create for the creator
console.log(`[MIXED PLAYLIST] Creating Spotify playlist for creator: "${playlistName}"`);
const creatorProfile = db.db.prepare('SELECT id FROM users WHERE id=?').get(creatorId);
const { id: creatorPlaylistId, url: creatorUrl, imageUrl: creatorImageUrl } = await createSpotifyPlaylist(creatorToken, creatorProfile.id, playlistName, playlistDesc);
console.log(`[MIXED PLAYLIST] Adding ${finalUris.length} tracks to creator playlist ${creatorPlaylistId}`);
await addTracks(creatorToken, creatorPlaylistId, finalUris);
console.log(`[MIXED PLAYLIST] Successfully added tracks to creator playlist`);
let partnerResult = {};
if (createForBoth) {
console.log(`[MIXED PLAYLIST] Creating playlist for partner as well: ${partnerId}`);
try {
const partnerToken = await ensureValidAccessToken(partnerId);
const partnerProfile = db.db.prepare('SELECT id FROM users WHERE id=?').get(partnerId);
if (!partnerProfile) {
throw new Error('Partner user not found');
}
console.log(`[MIXED PLAYLIST] Creating Spotify playlist for partner: "${playlistName}"`);
const partnerPlaylist = await createSpotifyPlaylist(partnerToken, partnerProfile.id, playlistName, playlistDesc);
partnerResult = partnerPlaylist;
console.log(`[MIXED PLAYLIST] Adding ${finalUris.length} tracks to partner playlist ${partnerResult.id}`);
await addTracks(partnerToken, partnerResult.id, finalUris);
console.log(`[MIXED PLAYLIST] Successfully created partner playlist: ${partnerResult.id}`);
}
catch (e) {
// Partner creation may fail (revoked consent, user not found, etc.); still return creator playlist
const errorMessage = e?.response?.data?.error?.message || e?.message || 'Unknown error';
console.error('[MIXED PLAYLIST] Partner playlist creation failed:', errorMessage);
partnerResult.error = errorMessage;
}
}
// Save playlist to database
const playlistId = uuidv4();
const now = Date.now();
console.log('[MIXED PLAYLIST] Saving playlist to database:', {
id: playlistId,
name: playlistName,
trackCount: finalUris.length,
creatorPlaylistId,
partnerPlaylistId: partnerResult.id,
imageUrl: creatorImageUrl,
trackUris: finalUris.slice(0, 3) // Log first 3 URIs for debugging
});
db.db.prepare(`
INSERT INTO mixed_playlists (
id, creator_id, partner_id, name, description, vibe, genres,
track_uris, creator_spotify_id, partner_spotify_id,
creator_spotify_url, partner_spotify_url, creator_spotify_image_url, partner_spotify_image_url,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(playlistId, creatorId, partnerId, playlistName, playlistDesc, vibe || null, JSON.stringify(genres || []), JSON.stringify(finalUris), creatorPlaylistId, partnerResult.id || null, creatorUrl, partnerResult.url || null, creatorImageUrl || null, partnerResult.imageUrl || null, now, now);
const response = {
id: playlistId,
name: playlistName,
description: playlistDesc,
trackUris: finalUris,
spotifyImageUrl: creatorImageUrl,
createdFor: {
creator: { playlistId: creatorPlaylistId, url: creatorUrl },
partner: partnerResult.id ? { playlistId: partnerResult.id, url: partnerResult.url } : null,
partnerError: partnerResult.error || null,
},
added: finalUris.length,
};
console.log('[MIXED PLAYLIST] Playlist creation completed successfully:', {
id: playlistId,
name: playlistName,
creatorPlaylistId,
partnerPlaylistId: partnerResult.id,
trackCount: finalUris.length,
imageUrl: creatorImageUrl
});
res.json(response);
}
catch (err) {
const detail = err?.response?.data || err?.message || 'Unknown error';
console.error('[MIXED PLAYLIST] Error during playlist creation:', detail);
console.error('[MIXED PLAYLIST] Full error object:', err);
res.status(500).json({ error: 'Failed to create mixed playlist', detail });
}
});

128
server/dist/routes/users.js vendored Normal file
View File

@ -0,0 +1,128 @@
import { Router } from 'express';
import { db } from '../lib/db.js';
import axios from 'axios';
import { requireAuth } from '../middleware/auth.js';
import { syncUserData } from '../lib/sync.js';
import { ensureValidAccessToken } from '../lib/spotify.js';
export const usersRouter = Router();
// Fetch and store user data (recently played, top tracks/artists)
usersRouter.post('/:uid/sync', requireAuth, async (req, res) => {
try {
const { uid } = req.params;
if (!req.user || req.user.uid !== uid)
return res.status(403).json({ error: 'Forbidden' });
await syncUserData(uid);
res.json({ ok: true });
}
catch (err) {
console.error(err.response?.data || err.message);
res.status(500).json({ error: 'Sync failed' });
}
});
usersRouter.get('/:uid', (req, res) => {
const { uid } = req.params;
const user = db.db.prepare('SELECT id, display_name, email, avatar_url, country, product, created_at, updated_at, last_synced_at FROM users WHERE id = ?').get(uid);
if (!user)
return res.status(404).json({ error: 'Not found' });
res.json(user);
});
// Simple search by display_name (substring, case-insensitive)
usersRouter.get('/search/q', (req, res) => {
const q = String(req.query.q || '').trim();
if (!q)
return res.json([]);
const rows = db.db
.prepare('SELECT id, display_name, avatar_url FROM users WHERE lower(display_name) LIKE ? LIMIT 20')
.all(`%${q.toLowerCase()}%`);
res.json(rows);
});
usersRouter.get('/:uid/recently-played', (req, res) => {
const { uid } = req.params;
const rows = db.db.prepare('SELECT id, user_id, played_at, track_json FROM recently_played WHERE user_id = ? ORDER BY played_at DESC LIMIT 50').all(uid);
res.json(rows.map((r) => ({ id: r.id, user_id: r.user_id, played_at: r.played_at, track: JSON.parse(r.track_json) })));
});
usersRouter.get('/:uid/top-tracks', (req, res) => {
const { uid } = req.params;
const range = req.query.time_range || 'short_term';
const rows = db.db.prepare('SELECT rank, track_json FROM top_tracks WHERE user_id = ? AND time_range = ? ORDER BY rank ASC').all(uid, range);
res.json(rows.map((r) => ({ rank: r.rank, track: JSON.parse(r.track_json) })));
});
usersRouter.get('/:uid/top-artists', (req, res) => {
const { uid } = req.params;
const range = req.query.time_range || 'short_term';
const rows = db.db.prepare('SELECT rank, artist_json FROM top_artists WHERE user_id = ? AND time_range = ? ORDER BY rank ASC').all(uid, range);
res.json(rows.map((r) => ({ rank: r.rank, artist: JSON.parse(r.artist_json) })));
});
// status: last sync time and next scheduled time (based on 10 min interval)
usersRouter.get('/:uid/status', (req, res) => {
const { uid } = req.params;
const row = db.db.prepare('SELECT last_synced_at FROM users WHERE id = ?').get(uid);
const last = row?.last_synced_at || null;
// Match backend full sync interval (2 minutes)
const next = last ? last + 2 * 60 * 1000 : null;
res.json({ lastSyncedAt: last, nextSyncAt: next });
});
// Now playing (with token refresh)
usersRouter.get('/:uid/now-playing', requireAuth, async (req, res) => {
try {
const { uid } = req.params;
if (!req.user)
return res.status(403).json({ error: 'Forbidden' });
if (req.user.uid !== uid) {
// allow if users are partnered
const paired = db.db.prepare('SELECT 1 FROM friendships WHERE (user_a_id=? AND user_b_id=?) OR (user_a_id=? AND user_b_id=?)').get(req.user.uid, uid, uid, req.user.uid);
if (!paired)
return res.status(403).json({ error: 'Forbidden' });
}
// Try DB-captured snapshot first (last 60s)
const recent = db.db.prepare('SELECT played_at, track_json FROM recently_played WHERE user_id=? ORDER BY played_at DESC LIMIT 1').get(uid);
if (recent?.played_at && Date.now() - recent.played_at < 60000 && recent?.track_json) {
try {
const item = JSON.parse(recent.track_json);
return res.json({ is_playing: true, item, source: 'db' });
}
catch { }
}
const accessToken = await ensureValidAccessToken(uid);
const resp = await axios.get('https://api.spotify.com/v1/me/player/currently-playing?additional_types=track', {
headers: { Authorization: `Bearer ${accessToken}` },
validateStatus: () => true,
});
if (resp.status === 204)
return res.json({ is_playing: false });
if (resp.status >= 400) {
// Return a benign payload rather than 500 to avoid breaking UI when Spotify returns errors
return res.json({ is_playing: false });
}
return res.status(200).json(resp.data);
}
catch (e) {
res.status(200).json({ is_playing: false });
}
});
// Audio features bulk
usersRouter.get('/:uid/audio-features', requireAuth, async (req, res) => {
try {
const { uid } = req.params;
const ids = String(req.query.ids || '').split(',').map(s => s.trim()).filter(Boolean);
if (!req.user)
return res.status(403).json({ error: 'Forbidden' });
if (req.user.uid !== uid) {
const paired = db.db.prepare('SELECT 1 FROM friendships WHERE (user_a_id=? AND user_b_id=?) OR (user_a_id=? AND user_b_id=?)').get(req.user.uid, uid, uid, req.user.uid);
if (!paired)
return res.status(403).json({ error: 'Forbidden' });
}
if (!ids.length)
return res.json({ audio_features: [] });
const accessToken = await ensureValidAccessToken(uid);
// Some tracks may be local or missing features; handle gracefully
const resp = await axios.get('https://api.spotify.com/v1/audio-features', {
headers: { Authorization: `Bearer ${accessToken}` },
params: { ids: ids.slice(0, 100).join(',') },
});
res.json({ audio_features: Array.isArray(resp.data?.audio_features) ? resp.data.audio_features.filter(Boolean) : [] });
}
catch (e) {
res.status(500).json({ error: 'Failed to fetch audio features', detail: e?.response?.data || e?.message });
}
});

1
server/node_modules/.bin/esbuild generated vendored Symbolic link
View File

@ -0,0 +1 @@
../esbuild/bin/esbuild

1
server/node_modules/.bin/mime generated vendored Symbolic link
View File

@ -0,0 +1 @@
../mime/cli.js

1
server/node_modules/.bin/prebuild-install generated vendored Symbolic link
View File

@ -0,0 +1 @@
../prebuild-install/bin.js

1
server/node_modules/.bin/rc generated vendored Symbolic link
View File

@ -0,0 +1 @@
../rc/cli.js

1
server/node_modules/.bin/semver generated vendored Symbolic link
View File

@ -0,0 +1 @@
../semver/bin/semver.js

1
server/node_modules/.bin/tsc generated vendored Symbolic link
View File

@ -0,0 +1 @@
../typescript/bin/tsc

1
server/node_modules/.bin/tsserver generated vendored Symbolic link
View File

@ -0,0 +1 @@
../typescript/bin/tsserver

1
server/node_modules/.bin/tsx generated vendored Symbolic link
View File

@ -0,0 +1 @@
../tsx/dist/cli.mjs

1
server/node_modules/.bin/uuid generated vendored Symbolic link
View File

@ -0,0 +1 @@
../uuid/dist-node/bin/uuid

1996
server/node_modules/.package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

3
server/node_modules/@esbuild/linux-x64/README.md generated vendored Normal file
View File

@ -0,0 +1,3 @@
# esbuild
This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.

BIN
server/node_modules/@esbuild/linux-x64/bin/esbuild generated vendored Executable file

Binary file not shown.

20
server/node_modules/@esbuild/linux-x64/package.json generated vendored Normal file
View File

@ -0,0 +1,20 @@
{
"name": "@esbuild/linux-x64",
"version": "0.25.10",
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=18"
},
"os": [
"linux"
],
"cpu": [
"x64"
]
}

82
server/node_modules/@img/colour/LICENSE.md generated vendored Normal file
View File

@ -0,0 +1,82 @@
# Licensing
## color
Copyright (c) 2012 Heather Arthur
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## color-convert
Copyright (c) 2011-2016 Heather Arthur <fayearthur@gmail.com>.
Copyright (c) 2016-2021 Josh Junon <josh@junon.me>.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## color-string
Copyright (c) 2011 Heather Arthur <fayearthur@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## color-name
The MIT License (MIT)
Copyright (c) 2015 Dmitry Ivanov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

15
server/node_modules/@img/colour/README.md generated vendored Normal file
View File

@ -0,0 +1,15 @@
# `@img/colour`
The latest version of the
[color](https://www.npmjs.com/package/color)
package is now ESM-only,
however some JavaScript runtimes do not yet support this,
which includes versions of Node.js prior to 20.19.0.
This package converts the `color` package and its dependencies,
all of which are MIT-licensed, to CommonJS.
- [color](https://www.npmjs.com/package/color)
- [color-convert](https://www.npmjs.com/package/color-convert)
- [color-string](https://www.npmjs.com/package/color-string)
- [color-name](https://www.npmjs.com/package/color-name)

1594
server/node_modules/@img/colour/color.cjs generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
server/node_modules/@img/colour/index.cjs generated vendored Normal file
View File

@ -0,0 +1 @@
module.exports = require("./color.cjs").default;

45
server/node_modules/@img/colour/package.json generated vendored Normal file
View File

@ -0,0 +1,45 @@
{
"name": "@img/colour",
"version": "1.0.0",
"description": "The ESM-only 'color' package made compatible for use with CommonJS runtimes",
"license": "MIT",
"main": "index.cjs",
"authors": [
"Heather Arthur <fayearthur@gmail.com>",
"Josh Junon <josh@junon.me>",
"Maxime Thirouin",
"Dyma Ywanov <dfcreative@gmail.com>",
"LitoMore (https://github.com/LitoMore)"
],
"engines": {
"node": ">=18"
},
"files": [
"color.cjs"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/lovell/colour.git"
},
"type": "commonjs",
"keywords": [
"color",
"colour",
"cjs",
"commonjs"
],
"scripts": {
"build": "esbuild node_modules/color/index.js --bundle --platform=node --outfile=color.cjs",
"test": "node --test"
},
"devDependencies": {
"color": "5.0.0",
"color-convert": "3.1.0",
"color-name": "2.0.0",
"color-string": "2.1.0",
"esbuild": "^0.25.9"
}
}

View File

@ -0,0 +1,46 @@
# `@img/sharp-libvips-linux-x64`
Prebuilt libvips and dependencies for use with sharp on Linux (glibc) x64.
## Licensing
This software contains third-party libraries
used under the terms of the following licences:
| Library | Used under the terms of |
|---------------|-----------------------------------------------------------------------------------------------------------|
| aom | BSD 2-Clause + [Alliance for Open Media Patent License 1.0](https://aomedia.org/license/patent-license/) |
| cairo | Mozilla Public License 2.0 |
| cgif | MIT Licence |
| expat | MIT Licence |
| fontconfig | [fontconfig Licence](https://gitlab.freedesktop.org/fontconfig/fontconfig/blob/main/COPYING) (BSD-like) |
| freetype | [freetype Licence](https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT) (BSD-like) |
| fribidi | LGPLv3 |
| glib | LGPLv3 |
| harfbuzz | MIT Licence |
| highway | Apache-2.0 License, BSD 3-Clause |
| lcms | MIT Licence |
| libarchive | BSD 2-Clause |
| libexif | LGPLv3 |
| libffi | MIT Licence |
| libheif | LGPLv3 |
| libimagequant | [BSD 2-Clause](https://github.com/lovell/libimagequant/blob/main/COPYRIGHT) |
| libnsgif | MIT Licence |
| libpng | [libpng License](https://github.com/pnggroup/libpng/blob/master/LICENSE) |
| librsvg | LGPLv3 |
| libspng | [BSD 2-Clause, libpng License](https://github.com/randy408/libspng/blob/master/LICENSE) |
| libtiff | [libtiff License](https://gitlab.com/libtiff/libtiff/blob/master/LICENSE.md) (BSD-like) |
| libvips | LGPLv3 |
| libwebp | New BSD License |
| libxml2 | MIT Licence |
| mozjpeg | [zlib License, IJG License, BSD-3-Clause](https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md) |
| pango | LGPLv3 |
| pixman | MIT Licence |
| proxy-libintl | LGPLv3 |
| zlib-ng | [zlib Licence](https://github.com/zlib-ng/zlib-ng/blob/develop/LICENSE.md) |
Use of libraries under the terms of the LGPLv3 is via the
"any later version" clause of the LGPLv2 or LGPLv2.1.
Please report any errors or omissions via
https://github.com/lovell/sharp-libvips/issues/new

View File

@ -0,0 +1,221 @@
/* glibconfig.h
*
* This is a generated file. Please modify 'glibconfig.h.in'
*/
#ifndef __GLIBCONFIG_H__
#define __GLIBCONFIG_H__
#include <glib/gmacros.h>
#include <limits.h>
#include <float.h>
#define GLIB_HAVE_ALLOCA_H
#define GLIB_STATIC_COMPILATION 1
#define GOBJECT_STATIC_COMPILATION 1
#define GIO_STATIC_COMPILATION 1
#define GMODULE_STATIC_COMPILATION 1
#define GI_STATIC_COMPILATION 1
#define G_INTL_STATIC_COMPILATION 1
#define FFI_STATIC_BUILD 1
/* Specifies that GLib's g_print*() functions wrap the
* system printf functions. This is useful to know, for example,
* when using glibc's register_printf_function().
*/
#define GLIB_USING_SYSTEM_PRINTF
G_BEGIN_DECLS
#define G_MINFLOAT FLT_MIN
#define G_MAXFLOAT FLT_MAX
#define G_MINDOUBLE DBL_MIN
#define G_MAXDOUBLE DBL_MAX
#define G_MINSHORT SHRT_MIN
#define G_MAXSHORT SHRT_MAX
#define G_MAXUSHORT USHRT_MAX
#define G_MININT INT_MIN
#define G_MAXINT INT_MAX
#define G_MAXUINT UINT_MAX
#define G_MINLONG LONG_MIN
#define G_MAXLONG LONG_MAX
#define G_MAXULONG ULONG_MAX
typedef signed char gint8;
typedef unsigned char guint8;
typedef signed short gint16;
typedef unsigned short guint16;
#define G_GINT16_MODIFIER "h"
#define G_GINT16_FORMAT "hi"
#define G_GUINT16_FORMAT "hu"
typedef signed int gint32;
typedef unsigned int guint32;
#define G_GINT32_MODIFIER ""
#define G_GINT32_FORMAT "i"
#define G_GUINT32_FORMAT "u"
#define G_HAVE_GINT64 1 /* deprecated, always true */
typedef signed long gint64;
typedef unsigned long guint64;
#define G_GINT64_CONSTANT(val) (val##L)
#define G_GUINT64_CONSTANT(val) (val##UL)
#define G_GINT64_MODIFIER "l"
#define G_GINT64_FORMAT "li"
#define G_GUINT64_FORMAT "lu"
#define GLIB_SIZEOF_VOID_P 8
#define GLIB_SIZEOF_LONG 8
#define GLIB_SIZEOF_SIZE_T 8
#define GLIB_SIZEOF_SSIZE_T 8
typedef signed long gssize;
typedef unsigned long gsize;
#define G_GSIZE_MODIFIER "l"
#define G_GSSIZE_MODIFIER "l"
#define G_GSIZE_FORMAT "lu"
#define G_GSSIZE_FORMAT "li"
#define G_MAXSIZE G_MAXULONG
#define G_MINSSIZE G_MINLONG
#define G_MAXSSIZE G_MAXLONG
typedef gint64 goffset;
#define G_MINOFFSET G_MININT64
#define G_MAXOFFSET G_MAXINT64
#define G_GOFFSET_MODIFIER G_GINT64_MODIFIER
#define G_GOFFSET_FORMAT G_GINT64_FORMAT
#define G_GOFFSET_CONSTANT(val) G_GINT64_CONSTANT(val)
#define G_POLLFD_FORMAT "%d"
#define GPOINTER_TO_INT(p) ((gint) (glong) (p))
#define GPOINTER_TO_UINT(p) ((guint) (gulong) (p))
#define GINT_TO_POINTER(i) ((gpointer) (glong) (i))
#define GUINT_TO_POINTER(u) ((gpointer) (gulong) (u))
typedef signed long gintptr;
typedef unsigned long guintptr;
#define G_GINTPTR_MODIFIER "l"
#define G_GINTPTR_FORMAT "li"
#define G_GUINTPTR_FORMAT "lu"
#define GLIB_MAJOR_VERSION 2
#define GLIB_MINOR_VERSION 86
#define GLIB_MICRO_VERSION 0
#define G_OS_UNIX
#define G_VA_COPY va_copy
#define G_VA_COPY_AS_ARRAY 1
#define G_HAVE_ISO_VARARGS 1
/* gcc-2.95.x supports both gnu style and ISO varargs, but if -ansi
* is passed ISO vararg support is turned off, and there is no work
* around to turn it on, so we unconditionally turn it off.
*/
#if __GNUC__ == 2 && __GNUC_MINOR__ == 95
# undef G_HAVE_ISO_VARARGS
#endif
#define G_HAVE_GROWING_STACK 0
#ifndef _MSC_VER
# define G_HAVE_GNUC_VARARGS 1
#endif
#if defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590)
#define G_GNUC_INTERNAL __attribute__((visibility("hidden")))
#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x550)
#define G_GNUC_INTERNAL __hidden
#elif defined (__GNUC__) && defined (G_HAVE_GNUC_VISIBILITY)
#define G_GNUC_INTERNAL __attribute__((visibility("hidden")))
#else
#define G_GNUC_INTERNAL
#endif
#define G_THREADS_ENABLED
#define G_THREADS_IMPL_POSIX
#define G_ATOMIC_LOCK_FREE
#define GINT16_TO_LE(val) ((gint16) (val))
#define GUINT16_TO_LE(val) ((guint16) (val))
#define GINT16_TO_BE(val) ((gint16) GUINT16_SWAP_LE_BE (val))
#define GUINT16_TO_BE(val) (GUINT16_SWAP_LE_BE (val))
#define GINT32_TO_LE(val) ((gint32) (val))
#define GUINT32_TO_LE(val) ((guint32) (val))
#define GINT32_TO_BE(val) ((gint32) GUINT32_SWAP_LE_BE (val))
#define GUINT32_TO_BE(val) (GUINT32_SWAP_LE_BE (val))
#define GINT64_TO_LE(val) ((gint64) (val))
#define GUINT64_TO_LE(val) ((guint64) (val))
#define GINT64_TO_BE(val) ((gint64) GUINT64_SWAP_LE_BE (val))
#define GUINT64_TO_BE(val) (GUINT64_SWAP_LE_BE (val))
#define GLONG_TO_LE(val) ((glong) GINT64_TO_LE (val))
#define GULONG_TO_LE(val) ((gulong) GUINT64_TO_LE (val))
#define GLONG_TO_BE(val) ((glong) GINT64_TO_BE (val))
#define GULONG_TO_BE(val) ((gulong) GUINT64_TO_BE (val))
#define GINT_TO_LE(val) ((gint) GINT32_TO_LE (val))
#define GUINT_TO_LE(val) ((guint) GUINT32_TO_LE (val))
#define GINT_TO_BE(val) ((gint) GINT32_TO_BE (val))
#define GUINT_TO_BE(val) ((guint) GUINT32_TO_BE (val))
#define GSIZE_TO_LE(val) ((gsize) GUINT64_TO_LE (val))
#define GSSIZE_TO_LE(val) ((gssize) GINT64_TO_LE (val))
#define GSIZE_TO_BE(val) ((gsize) GUINT64_TO_BE (val))
#define GSSIZE_TO_BE(val) ((gssize) GINT64_TO_BE (val))
#define G_BYTE_ORDER G_LITTLE_ENDIAN
#define GLIB_SYSDEF_POLLIN =1
#define GLIB_SYSDEF_POLLOUT =4
#define GLIB_SYSDEF_POLLPRI =2
#define GLIB_SYSDEF_POLLHUP =16
#define GLIB_SYSDEF_POLLERR =8
#define GLIB_SYSDEF_POLLNVAL =32
/* No way to disable deprecation warnings for macros, so only emit deprecation
* warnings on platforms where usage of this macro is broken */
#if defined(__APPLE__) || defined(_MSC_VER) || defined(__CYGWIN__)
#define G_MODULE_SUFFIX "so" GLIB_DEPRECATED_MACRO_IN_2_76
#else
#define G_MODULE_SUFFIX "so"
#endif
typedef int GPid;
#define G_PID_FORMAT "i"
#define GLIB_SYSDEF_AF_UNIX 1
#define GLIB_SYSDEF_AF_INET 2
#define GLIB_SYSDEF_AF_INET6 10
#define GLIB_SYSDEF_MSG_OOB 1
#define GLIB_SYSDEF_MSG_PEEK 2
#define GLIB_SYSDEF_MSG_DONTROUTE 4
#define G_DIR_SEPARATOR '/'
#define G_DIR_SEPARATOR_S "/"
#define G_SEARCHPATH_SEPARATOR ':'
#define G_SEARCHPATH_SEPARATOR_S ":"
#undef G_HAVE_FREE_SIZED
G_END_DECLS
#endif /* __GLIBCONFIG_H__ */

View File

@ -0,0 +1 @@
module.exports = __dirname;

Binary file not shown.

View File

@ -0,0 +1,42 @@
{
"name": "@img/sharp-libvips-linux-x64",
"version": "1.2.3",
"description": "Prebuilt libvips and dependencies for use with sharp on Linux (glibc) x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
"repository": {
"type": "git",
"url": "git+https://github.com/lovell/sharp-libvips.git",
"directory": "npm/linux-x64"
},
"license": "LGPL-3.0-or-later",
"funding": {
"url": "https://opencollective.com/libvips"
},
"preferUnplugged": true,
"publishConfig": {
"access": "public"
},
"files": [
"lib",
"versions.json"
],
"type": "commonjs",
"exports": {
"./lib": "./lib/index.js",
"./package": "./package.json",
"./versions": "./versions.json"
},
"config": {
"glibc": ">=2.26"
},
"os": [
"linux"
],
"libc": [
"glibc"
],
"cpu": [
"x64"
]
}

View File

@ -0,0 +1,30 @@
{
"aom": "3.13.1",
"archive": "3.8.1",
"cairo": "1.18.4",
"cgif": "0.5.0",
"exif": "0.6.25",
"expat": "2.7.2",
"ffi": "3.5.2",
"fontconfig": "2.17.1",
"freetype": "2.14.1",
"fribidi": "1.0.16",
"glib": "2.86.0",
"harfbuzz": "11.5.0",
"heif": "1.20.2",
"highway": "1.3.0",
"imagequant": "2.4.1",
"lcms": "2.17",
"mozjpeg": "4.1.5",
"pango": "1.57.0",
"pixman": "0.46.4",
"png": "1.6.50",
"proxy-libintl": "0.5",
"rsvg": "2.61.1",
"spng": "0.7.4",
"tiff": "4.7.0",
"vips": "8.17.2",
"webp": "1.6.0",
"xml2": "2.15.0",
"zlib-ng": "2.2.5"
}

View File

@ -0,0 +1,46 @@
# `@img/sharp-libvips-linuxmusl-x64`
Prebuilt libvips and dependencies for use with sharp on Linux (musl) x64.
## Licensing
This software contains third-party libraries
used under the terms of the following licences:
| Library | Used under the terms of |
|---------------|-----------------------------------------------------------------------------------------------------------|
| aom | BSD 2-Clause + [Alliance for Open Media Patent License 1.0](https://aomedia.org/license/patent-license/) |
| cairo | Mozilla Public License 2.0 |
| cgif | MIT Licence |
| expat | MIT Licence |
| fontconfig | [fontconfig Licence](https://gitlab.freedesktop.org/fontconfig/fontconfig/blob/main/COPYING) (BSD-like) |
| freetype | [freetype Licence](https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT) (BSD-like) |
| fribidi | LGPLv3 |
| glib | LGPLv3 |
| harfbuzz | MIT Licence |
| highway | Apache-2.0 License, BSD 3-Clause |
| lcms | MIT Licence |
| libarchive | BSD 2-Clause |
| libexif | LGPLv3 |
| libffi | MIT Licence |
| libheif | LGPLv3 |
| libimagequant | [BSD 2-Clause](https://github.com/lovell/libimagequant/blob/main/COPYRIGHT) |
| libnsgif | MIT Licence |
| libpng | [libpng License](https://github.com/pnggroup/libpng/blob/master/LICENSE) |
| librsvg | LGPLv3 |
| libspng | [BSD 2-Clause, libpng License](https://github.com/randy408/libspng/blob/master/LICENSE) |
| libtiff | [libtiff License](https://gitlab.com/libtiff/libtiff/blob/master/LICENSE.md) (BSD-like) |
| libvips | LGPLv3 |
| libwebp | New BSD License |
| libxml2 | MIT Licence |
| mozjpeg | [zlib License, IJG License, BSD-3-Clause](https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md) |
| pango | LGPLv3 |
| pixman | MIT Licence |
| proxy-libintl | LGPLv3 |
| zlib-ng | [zlib Licence](https://github.com/zlib-ng/zlib-ng/blob/develop/LICENSE.md) |
Use of libraries under the terms of the LGPLv3 is via the
"any later version" clause of the LGPLv2 or LGPLv2.1.
Please report any errors or omissions via
https://github.com/lovell/sharp-libvips/issues/new

View File

@ -0,0 +1,221 @@
/* glibconfig.h
*
* This is a generated file. Please modify 'glibconfig.h.in'
*/
#ifndef __GLIBCONFIG_H__
#define __GLIBCONFIG_H__
#include <glib/gmacros.h>
#include <limits.h>
#include <float.h>
#define GLIB_HAVE_ALLOCA_H
#define GLIB_STATIC_COMPILATION 1
#define GOBJECT_STATIC_COMPILATION 1
#define GIO_STATIC_COMPILATION 1
#define GMODULE_STATIC_COMPILATION 1
#define GI_STATIC_COMPILATION 1
#define G_INTL_STATIC_COMPILATION 1
#define FFI_STATIC_BUILD 1
/* Specifies that GLib's g_print*() functions wrap the
* system printf functions. This is useful to know, for example,
* when using glibc's register_printf_function().
*/
#define GLIB_USING_SYSTEM_PRINTF
G_BEGIN_DECLS
#define G_MINFLOAT FLT_MIN
#define G_MAXFLOAT FLT_MAX
#define G_MINDOUBLE DBL_MIN
#define G_MAXDOUBLE DBL_MAX
#define G_MINSHORT SHRT_MIN
#define G_MAXSHORT SHRT_MAX
#define G_MAXUSHORT USHRT_MAX
#define G_MININT INT_MIN
#define G_MAXINT INT_MAX
#define G_MAXUINT UINT_MAX
#define G_MINLONG LONG_MIN
#define G_MAXLONG LONG_MAX
#define G_MAXULONG ULONG_MAX
typedef signed char gint8;
typedef unsigned char guint8;
typedef signed short gint16;
typedef unsigned short guint16;
#define G_GINT16_MODIFIER "h"
#define G_GINT16_FORMAT "hi"
#define G_GUINT16_FORMAT "hu"
typedef signed int gint32;
typedef unsigned int guint32;
#define G_GINT32_MODIFIER ""
#define G_GINT32_FORMAT "i"
#define G_GUINT32_FORMAT "u"
#define G_HAVE_GINT64 1 /* deprecated, always true */
typedef signed long gint64;
typedef unsigned long guint64;
#define G_GINT64_CONSTANT(val) (val##L)
#define G_GUINT64_CONSTANT(val) (val##UL)
#define G_GINT64_MODIFIER "l"
#define G_GINT64_FORMAT "li"
#define G_GUINT64_FORMAT "lu"
#define GLIB_SIZEOF_VOID_P 8
#define GLIB_SIZEOF_LONG 8
#define GLIB_SIZEOF_SIZE_T 8
#define GLIB_SIZEOF_SSIZE_T 8
typedef signed long gssize;
typedef unsigned long gsize;
#define G_GSIZE_MODIFIER "l"
#define G_GSSIZE_MODIFIER "l"
#define G_GSIZE_FORMAT "lu"
#define G_GSSIZE_FORMAT "li"
#define G_MAXSIZE G_MAXULONG
#define G_MINSSIZE G_MINLONG
#define G_MAXSSIZE G_MAXLONG
typedef gint64 goffset;
#define G_MINOFFSET G_MININT64
#define G_MAXOFFSET G_MAXINT64
#define G_GOFFSET_MODIFIER G_GINT64_MODIFIER
#define G_GOFFSET_FORMAT G_GINT64_FORMAT
#define G_GOFFSET_CONSTANT(val) G_GINT64_CONSTANT(val)
#define G_POLLFD_FORMAT "%d"
#define GPOINTER_TO_INT(p) ((gint) (glong) (p))
#define GPOINTER_TO_UINT(p) ((guint) (gulong) (p))
#define GINT_TO_POINTER(i) ((gpointer) (glong) (i))
#define GUINT_TO_POINTER(u) ((gpointer) (gulong) (u))
typedef signed long gintptr;
typedef unsigned long guintptr;
#define G_GINTPTR_MODIFIER "l"
#define G_GINTPTR_FORMAT "li"
#define G_GUINTPTR_FORMAT "lu"
#define GLIB_MAJOR_VERSION 2
#define GLIB_MINOR_VERSION 86
#define GLIB_MICRO_VERSION 0
#define G_OS_UNIX
#define G_VA_COPY va_copy
#define G_VA_COPY_AS_ARRAY 1
#define G_HAVE_ISO_VARARGS 1
/* gcc-2.95.x supports both gnu style and ISO varargs, but if -ansi
* is passed ISO vararg support is turned off, and there is no work
* around to turn it on, so we unconditionally turn it off.
*/
#if __GNUC__ == 2 && __GNUC_MINOR__ == 95
# undef G_HAVE_ISO_VARARGS
#endif
#define G_HAVE_GROWING_STACK 0
#ifndef _MSC_VER
# define G_HAVE_GNUC_VARARGS 1
#endif
#if defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590)
#define G_GNUC_INTERNAL __attribute__((visibility("hidden")))
#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x550)
#define G_GNUC_INTERNAL __hidden
#elif defined (__GNUC__) && defined (G_HAVE_GNUC_VISIBILITY)
#define G_GNUC_INTERNAL __attribute__((visibility("hidden")))
#else
#define G_GNUC_INTERNAL
#endif
#define G_THREADS_ENABLED
#define G_THREADS_IMPL_POSIX
#define G_ATOMIC_LOCK_FREE
#define GINT16_TO_LE(val) ((gint16) (val))
#define GUINT16_TO_LE(val) ((guint16) (val))
#define GINT16_TO_BE(val) ((gint16) GUINT16_SWAP_LE_BE (val))
#define GUINT16_TO_BE(val) (GUINT16_SWAP_LE_BE (val))
#define GINT32_TO_LE(val) ((gint32) (val))
#define GUINT32_TO_LE(val) ((guint32) (val))
#define GINT32_TO_BE(val) ((gint32) GUINT32_SWAP_LE_BE (val))
#define GUINT32_TO_BE(val) (GUINT32_SWAP_LE_BE (val))
#define GINT64_TO_LE(val) ((gint64) (val))
#define GUINT64_TO_LE(val) ((guint64) (val))
#define GINT64_TO_BE(val) ((gint64) GUINT64_SWAP_LE_BE (val))
#define GUINT64_TO_BE(val) (GUINT64_SWAP_LE_BE (val))
#define GLONG_TO_LE(val) ((glong) GINT64_TO_LE (val))
#define GULONG_TO_LE(val) ((gulong) GUINT64_TO_LE (val))
#define GLONG_TO_BE(val) ((glong) GINT64_TO_BE (val))
#define GULONG_TO_BE(val) ((gulong) GUINT64_TO_BE (val))
#define GINT_TO_LE(val) ((gint) GINT32_TO_LE (val))
#define GUINT_TO_LE(val) ((guint) GUINT32_TO_LE (val))
#define GINT_TO_BE(val) ((gint) GINT32_TO_BE (val))
#define GUINT_TO_BE(val) ((guint) GUINT32_TO_BE (val))
#define GSIZE_TO_LE(val) ((gsize) GUINT64_TO_LE (val))
#define GSSIZE_TO_LE(val) ((gssize) GINT64_TO_LE (val))
#define GSIZE_TO_BE(val) ((gsize) GUINT64_TO_BE (val))
#define GSSIZE_TO_BE(val) ((gssize) GINT64_TO_BE (val))
#define G_BYTE_ORDER G_LITTLE_ENDIAN
#define GLIB_SYSDEF_POLLIN =1
#define GLIB_SYSDEF_POLLOUT =4
#define GLIB_SYSDEF_POLLPRI =2
#define GLIB_SYSDEF_POLLHUP =16
#define GLIB_SYSDEF_POLLERR =8
#define GLIB_SYSDEF_POLLNVAL =32
/* No way to disable deprecation warnings for macros, so only emit deprecation
* warnings on platforms where usage of this macro is broken */
#if defined(__APPLE__) || defined(_MSC_VER) || defined(__CYGWIN__)
#define G_MODULE_SUFFIX "so" GLIB_DEPRECATED_MACRO_IN_2_76
#else
#define G_MODULE_SUFFIX "so"
#endif
typedef int GPid;
#define G_PID_FORMAT "i"
#define GLIB_SYSDEF_AF_UNIX 1
#define GLIB_SYSDEF_AF_INET 2
#define GLIB_SYSDEF_AF_INET6 10
#define GLIB_SYSDEF_MSG_OOB 1
#define GLIB_SYSDEF_MSG_PEEK 2
#define GLIB_SYSDEF_MSG_DONTROUTE 4
#define G_DIR_SEPARATOR '/'
#define G_DIR_SEPARATOR_S "/"
#define G_SEARCHPATH_SEPARATOR ':'
#define G_SEARCHPATH_SEPARATOR_S ":"
#undef G_HAVE_FREE_SIZED
G_END_DECLS
#endif /* __GLIBCONFIG_H__ */

View File

@ -0,0 +1 @@
module.exports = __dirname;

Binary file not shown.

View File

@ -0,0 +1,42 @@
{
"name": "@img/sharp-libvips-linuxmusl-x64",
"version": "1.2.3",
"description": "Prebuilt libvips and dependencies for use with sharp on Linux (musl) x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
"repository": {
"type": "git",
"url": "git+https://github.com/lovell/sharp-libvips.git",
"directory": "npm/linuxmusl-x64"
},
"license": "LGPL-3.0-or-later",
"funding": {
"url": "https://opencollective.com/libvips"
},
"preferUnplugged": true,
"publishConfig": {
"access": "public"
},
"files": [
"lib",
"versions.json"
],
"type": "commonjs",
"exports": {
"./lib": "./lib/index.js",
"./package": "./package.json",
"./versions": "./versions.json"
},
"config": {
"musl": ">=1.2.2"
},
"os": [
"linux"
],
"libc": [
"musl"
],
"cpu": [
"x64"
]
}

View File

@ -0,0 +1,30 @@
{
"aom": "3.13.1",
"archive": "3.8.1",
"cairo": "1.18.4",
"cgif": "0.5.0",
"exif": "0.6.25",
"expat": "2.7.2",
"ffi": "3.5.2",
"fontconfig": "2.17.1",
"freetype": "2.14.1",
"fribidi": "1.0.16",
"glib": "2.86.0",
"harfbuzz": "11.5.0",
"heif": "1.20.2",
"highway": "1.3.0",
"imagequant": "2.4.1",
"lcms": "2.17",
"mozjpeg": "4.1.5",
"pango": "1.57.0",
"pixman": "0.46.4",
"png": "1.6.50",
"proxy-libintl": "0.5",
"rsvg": "2.61.1",
"spng": "0.7.4",
"tiff": "4.7.0",
"vips": "8.17.2",
"webp": "1.6.0",
"xml2": "2.15.0",
"zlib-ng": "2.2.5"
}

191
server/node_modules/@img/sharp-linux-x64/LICENSE generated vendored Normal file
View File

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

18
server/node_modules/@img/sharp-linux-x64/README.md generated vendored Normal file
View File

@ -0,0 +1,18 @@
# `@img/sharp-linux-x64`
Prebuilt sharp for use with Linux (glibc) x64.
## Licensing
Copyright 2013 Lovell Fuller and others.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

46
server/node_modules/@img/sharp-linux-x64/package.json generated vendored Normal file
View File

@ -0,0 +1,46 @@
{
"name": "@img/sharp-linux-x64",
"version": "0.34.4",
"description": "Prebuilt sharp for use with Linux (glibc) x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
"repository": {
"type": "git",
"url": "git+https://github.com/lovell/sharp.git",
"directory": "npm/linux-x64"
},
"license": "Apache-2.0",
"funding": {
"url": "https://opencollective.com/libvips"
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.3"
},
"files": [
"lib"
],
"publishConfig": {
"access": "public"
},
"type": "commonjs",
"exports": {
"./sharp.node": "./lib/sharp-linux-x64.node",
"./package": "./package.json"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"config": {
"glibc": ">=2.26"
},
"os": [
"linux"
],
"libc": [
"glibc"
],
"cpu": [
"x64"
]
}

191
server/node_modules/@img/sharp-linuxmusl-x64/LICENSE generated vendored Normal file
View File

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

18
server/node_modules/@img/sharp-linuxmusl-x64/README.md generated vendored Normal file
View File

@ -0,0 +1,18 @@
# `@img/sharp-linuxmusl-x64`
Prebuilt sharp for use with Linux (musl) x64.
## Licensing
Copyright 2013 Lovell Fuller and others.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

View File

@ -0,0 +1,46 @@
{
"name": "@img/sharp-linuxmusl-x64",
"version": "0.34.4",
"description": "Prebuilt sharp for use with Linux (musl) x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
"repository": {
"type": "git",
"url": "git+https://github.com/lovell/sharp.git",
"directory": "npm/linuxmusl-x64"
},
"license": "Apache-2.0",
"funding": {
"url": "https://opencollective.com/libvips"
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.3"
},
"files": [
"lib"
],
"publishConfig": {
"access": "public"
},
"type": "commonjs",
"exports": {
"./sharp.node": "./lib/sharp-linuxmusl-x64.node",
"./package": "./package.json"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"config": {
"musl": ">=1.2.2"
},
"os": [
"linux"
],
"libc": [
"musl"
],
"cpu": [
"x64"
]
}

21
server/node_modules/@types/better-sqlite3/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

15
server/node_modules/@types/better-sqlite3/README.md generated vendored Normal file
View File

@ -0,0 +1,15 @@
# Installation
> `npm install --save @types/better-sqlite3`
# Summary
This package contains type definitions for better-sqlite3 (https://github.com/JoshuaWise/better-sqlite3).
# Details
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/better-sqlite3.
### Additional Details
* Last updated: Fri, 04 Apr 2025 18:38:14 GMT
* Dependencies: [@types/node](https://npmjs.com/package/@types/node)
# Credits
These definitions were written by [Ben Davies](https://github.com/Morfent), [Mathew Rumsey](https://github.com/matrumz), [Santiago Aguilar](https://github.com/sant123), [Alessandro Vergani](https://github.com/loghorn), [Andrew Kaiser](https://github.com/andykais), [Mark Stewart](https://github.com/mrkstwrt), [Florian Stamer](https://github.com/stamerf), and [Beeno Tung](https://github.com/beenotung).

159
server/node_modules/@types/better-sqlite3/index.d.ts generated vendored Normal file
View File

@ -0,0 +1,159 @@
/// <reference types="node" />
// FIXME: Is this `any` really necessary?
type VariableArgFunction = (...params: any[]) => unknown;
type ArgumentTypes<F extends VariableArgFunction> = F extends (...args: infer A) => unknown ? A : never;
type ElementOf<T> = T extends Array<infer E> ? E : T;
declare namespace BetterSqlite3 {
interface Statement<BindParameters extends unknown[], Result = unknown> {
database: Database;
source: string;
reader: boolean;
readonly: boolean;
busy: boolean;
run(...params: BindParameters): Database.RunResult;
get(...params: BindParameters): Result | undefined;
all(...params: BindParameters): Result[];
iterate(...params: BindParameters): IterableIterator<Result>;
pluck(toggleState?: boolean): this;
expand(toggleState?: boolean): this;
raw(toggleState?: boolean): this;
bind(...params: BindParameters): this;
columns(): ColumnDefinition[];
safeIntegers(toggleState?: boolean): this;
}
interface ColumnDefinition {
name: string;
column: string | null;
table: string | null;
database: string | null;
type: string | null;
}
interface Transaction<F extends VariableArgFunction> {
(...params: ArgumentTypes<F>): ReturnType<F>;
default(...params: ArgumentTypes<F>): ReturnType<F>;
deferred(...params: ArgumentTypes<F>): ReturnType<F>;
immediate(...params: ArgumentTypes<F>): ReturnType<F>;
exclusive(...params: ArgumentTypes<F>): ReturnType<F>;
}
interface VirtualTableOptions {
rows: (...params: unknown[]) => Generator;
columns: string[];
parameters?: string[] | undefined;
safeIntegers?: boolean | undefined;
directOnly?: boolean | undefined;
}
interface Database {
memory: boolean;
readonly: boolean;
name: string;
open: boolean;
inTransaction: boolean;
prepare<BindParameters extends unknown[] | {} = unknown[], Result = unknown>(
source: string,
): BindParameters extends unknown[] ? Statement<BindParameters, Result> : Statement<[BindParameters], Result>;
transaction<F extends VariableArgFunction>(fn: F): Transaction<F>;
exec(source: string): this;
pragma(source: string, options?: Database.PragmaOptions): unknown;
function(
name: string,
cb: (...params: any[]) => any,
): this;
function(
name: string,
options: Database.RegistrationOptions,
cb: (...params: any[]) => any,
): this;
aggregate<T>(
name: string,
options: Database.RegistrationOptions & {
start?: T | (() => T);
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
step: (total: T, next: ElementOf<T>) => T | void;
inverse?: ((total: T, dropped: T) => T) | undefined;
result?: ((total: T) => unknown) | undefined;
},
): this;
loadExtension(path: string): this;
close(): this;
defaultSafeIntegers(toggleState?: boolean): this;
backup(destinationFile: string, options?: Database.BackupOptions): Promise<Database.BackupMetadata>;
table(name: string, options: VirtualTableOptions): this;
unsafeMode(unsafe?: boolean): this;
serialize(options?: Database.SerializeOptions): Buffer;
}
interface DatabaseConstructor {
new(filename?: string | Buffer, options?: Database.Options): Database;
(filename?: string, options?: Database.Options): Database;
prototype: Database;
SqliteError: SqliteErrorType;
}
}
declare class SqliteErrorClass extends Error {
name: string;
message: string;
code: string;
constructor(message: string, code: string);
}
type SqliteErrorType = typeof SqliteErrorClass;
declare namespace Database {
interface RunResult {
changes: number;
lastInsertRowid: number | bigint;
}
interface Options {
readonly?: boolean | undefined;
fileMustExist?: boolean | undefined;
timeout?: number | undefined;
verbose?: ((message?: unknown, ...additionalArgs: unknown[]) => void) | undefined;
nativeBinding?: string | undefined;
}
interface SerializeOptions {
attached?: string;
}
interface PragmaOptions {
simple?: boolean | undefined;
}
interface RegistrationOptions {
varargs?: boolean | undefined;
deterministic?: boolean | undefined;
safeIntegers?: boolean | undefined;
directOnly?: boolean | undefined;
}
type AggregateOptions = Parameters<BetterSqlite3.Database["aggregate"]>[1];
interface BackupMetadata {
totalPages: number;
remainingPages: number;
}
interface BackupOptions {
progress: (info: BackupMetadata) => number;
}
type SqliteError = SqliteErrorType;
type Statement<BindParameters extends unknown[] | {} = unknown[], Result = unknown> = BindParameters extends
unknown[] ? BetterSqlite3.Statement<BindParameters, Result>
: BetterSqlite3.Statement<[BindParameters], Result>;
type ColumnDefinition = BetterSqlite3.ColumnDefinition;
type Transaction<T extends VariableArgFunction = VariableArgFunction> = BetterSqlite3.Transaction<T>;
type Database = BetterSqlite3.Database;
}
declare const Database: BetterSqlite3.DatabaseConstructor;
export = Database;

63
server/node_modules/@types/better-sqlite3/package.json generated vendored Normal file
View File

@ -0,0 +1,63 @@
{
"name": "@types/better-sqlite3",
"version": "7.6.13",
"description": "TypeScript definitions for better-sqlite3",
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/better-sqlite3",
"license": "MIT",
"contributors": [
{
"name": "Ben Davies",
"githubUsername": "Morfent",
"url": "https://github.com/Morfent"
},
{
"name": "Mathew Rumsey",
"githubUsername": "matrumz",
"url": "https://github.com/matrumz"
},
{
"name": "Santiago Aguilar",
"githubUsername": "sant123",
"url": "https://github.com/sant123"
},
{
"name": "Alessandro Vergani",
"githubUsername": "loghorn",
"url": "https://github.com/loghorn"
},
{
"name": "Andrew Kaiser",
"githubUsername": "andykais",
"url": "https://github.com/andykais"
},
{
"name": "Mark Stewart",
"githubUsername": "mrkstwrt",
"url": "https://github.com/mrkstwrt"
},
{
"name": "Florian Stamer",
"githubUsername": "stamerf",
"url": "https://github.com/stamerf"
},
{
"name": "Beeno Tung",
"githubUsername": "beenotung",
"url": "https://github.com/beenotung"
}
],
"main": "",
"types": "index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"directory": "types/better-sqlite3"
},
"scripts": {},
"dependencies": {
"@types/node": "*"
},
"peerDependencies": {},
"typesPublisherContentHash": "d3573b172a1ed98c5e63d112a1d270bef2b683e7f13829155088694326b5fbac",
"typeScriptVersion": "5.1"
}

21
server/node_modules/@types/body-parser/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

15
server/node_modules/@types/body-parser/README.md generated vendored Normal file
View File

@ -0,0 +1,15 @@
# Installation
> `npm install --save @types/body-parser`
# Summary
This package contains type definitions for body-parser (https://github.com/expressjs/body-parser).
# Details
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/body-parser.
### Additional Details
* Last updated: Sat, 07 Jun 2025 02:15:25 GMT
* Dependencies: [@types/connect](https://npmjs.com/package/@types/connect), [@types/node](https://npmjs.com/package/@types/node)
# Credits
These definitions were written by [Santi Albo](https://github.com/santialbo), [Vilic Vane](https://github.com/vilic), [Jonathan Häberle](https://github.com/dreampulse), [Gevik Babakhani](https://github.com/blendsdk), [Tomasz Łaziuk](https://github.com/tlaziuk), [Jason Walton](https://github.com/jwalton), [Piotr Błażejewicz](https://github.com/peterblazejewicz), and [Sebastian Beltran](https://github.com/bjohansebas).

95
server/node_modules/@types/body-parser/index.d.ts generated vendored Normal file
View File

@ -0,0 +1,95 @@
/// <reference types="node" />
import { NextHandleFunction } from "connect";
import * as http from "http";
// for docs go to https://github.com/expressjs/body-parser/tree/1.19.0#body-parser
declare namespace bodyParser {
interface BodyParser {
/**
* @deprecated use individual json/urlencoded middlewares
*/
(options?: OptionsJson & OptionsText & OptionsUrlencoded): NextHandleFunction;
/**
* Returns middleware that only parses json and only looks at requests
* where the Content-Type header matches the type option.
*/
json(options?: OptionsJson): NextHandleFunction;
/**
* Returns middleware that parses all bodies as a Buffer and only looks at requests
* where the Content-Type header matches the type option.
*/
raw(options?: Options): NextHandleFunction;
/**
* Returns middleware that parses all bodies as a string and only looks at requests
* where the Content-Type header matches the type option.
*/
text(options?: OptionsText): NextHandleFunction;
/**
* Returns middleware that only parses urlencoded bodies and only looks at requests
* where the Content-Type header matches the type option
*/
urlencoded(options?: OptionsUrlencoded): NextHandleFunction;
}
interface Options {
/** When set to true, then deflated (compressed) bodies will be inflated; when false, deflated bodies are rejected. Defaults to true. */
inflate?: boolean | undefined;
/**
* Controls the maximum request body size. If this is a number,
* then the value specifies the number of bytes; if it is a string,
* the value is passed to the bytes library for parsing. Defaults to '100kb'.
*/
limit?: number | string | undefined;
/**
* The type option is used to determine what media type the middleware will parse
*/
type?: string | string[] | ((req: http.IncomingMessage) => any) | undefined;
/**
* The verify option, if supplied, is called as verify(req, res, buf, encoding),
* where buf is a Buffer of the raw request body and encoding is the encoding of the request.
*/
verify?(req: http.IncomingMessage, res: http.ServerResponse, buf: Buffer, encoding: string): void;
}
interface OptionsJson extends Options {
/**
* The reviver option is passed directly to JSON.parse as the second argument.
*/
reviver?(key: string, value: any): any;
/**
* When set to `true`, will only accept arrays and objects;
* when `false` will accept anything JSON.parse accepts. Defaults to `true`.
*/
strict?: boolean | undefined;
}
interface OptionsText extends Options {
/**
* Specify the default character set for the text content if the charset
* is not specified in the Content-Type header of the request.
* Defaults to `utf-8`.
*/
defaultCharset?: string | undefined;
}
interface OptionsUrlencoded extends Options {
/**
* The extended option allows to choose between parsing the URL-encoded data
* with the querystring library (when `false`) or the qs library (when `true`).
*/
extended?: boolean | undefined;
/**
* The parameterLimit option controls the maximum number of parameters
* that are allowed in the URL-encoded data. If a request contains more parameters than this value,
* a 413 will be returned to the client. Defaults to 1000.
*/
parameterLimit?: number | undefined;
}
}
declare const bodyParser: bodyParser.BodyParser;
export = bodyParser;

64
server/node_modules/@types/body-parser/package.json generated vendored Normal file
View File

@ -0,0 +1,64 @@
{
"name": "@types/body-parser",
"version": "1.19.6",
"description": "TypeScript definitions for body-parser",
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/body-parser",
"license": "MIT",
"contributors": [
{
"name": "Santi Albo",
"githubUsername": "santialbo",
"url": "https://github.com/santialbo"
},
{
"name": "Vilic Vane",
"githubUsername": "vilic",
"url": "https://github.com/vilic"
},
{
"name": "Jonathan Häberle",
"githubUsername": "dreampulse",
"url": "https://github.com/dreampulse"
},
{
"name": "Gevik Babakhani",
"githubUsername": "blendsdk",
"url": "https://github.com/blendsdk"
},
{
"name": "Tomasz Łaziuk",
"githubUsername": "tlaziuk",
"url": "https://github.com/tlaziuk"
},
{
"name": "Jason Walton",
"githubUsername": "jwalton",
"url": "https://github.com/jwalton"
},
{
"name": "Piotr Błażejewicz",
"githubUsername": "peterblazejewicz",
"url": "https://github.com/peterblazejewicz"
},
{
"name": "Sebastian Beltran",
"githubUsername": "bjohansebas",
"url": "https://github.com/bjohansebas"
}
],
"main": "",
"types": "index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"directory": "types/body-parser"
},
"scripts": {},
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
},
"peerDependencies": {},
"typesPublisherContentHash": "d788c843f427d6ca19640ee90eb433324a18f23aed05402a82c4e47e6d60b29d",
"typeScriptVersion": "5.1"
}

21
server/node_modules/@types/connect/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

15
server/node_modules/@types/connect/README.md generated vendored Normal file
View File

@ -0,0 +1,15 @@
# Installation
> `npm install --save @types/connect`
# Summary
This package contains type definitions for connect (https://github.com/senchalabs/connect).
# Details
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/connect.
### Additional Details
* Last updated: Mon, 06 Nov 2023 22:41:05 GMT
* Dependencies: [@types/node](https://npmjs.com/package/@types/node)
# Credits
These definitions were written by [Maxime LUCE](https://github.com/SomaticIT), and [Evan Hahn](https://github.com/EvanHahn).

91
server/node_modules/@types/connect/index.d.ts generated vendored Normal file
View File

@ -0,0 +1,91 @@
/// <reference types="node" />
import * as http from "http";
/**
* Create a new connect server.
*/
declare function createServer(): createServer.Server;
declare namespace createServer {
export type ServerHandle = HandleFunction | http.Server;
export class IncomingMessage extends http.IncomingMessage {
originalUrl?: http.IncomingMessage["url"] | undefined;
}
type NextFunction = (err?: any) => void;
export type SimpleHandleFunction = (req: IncomingMessage, res: http.ServerResponse) => void;
export type NextHandleFunction = (req: IncomingMessage, res: http.ServerResponse, next: NextFunction) => void;
export type ErrorHandleFunction = (
err: any,
req: IncomingMessage,
res: http.ServerResponse,
next: NextFunction,
) => void;
export type HandleFunction = SimpleHandleFunction | NextHandleFunction | ErrorHandleFunction;
export interface ServerStackItem {
route: string;
handle: ServerHandle;
}
export interface Server extends NodeJS.EventEmitter {
(req: http.IncomingMessage, res: http.ServerResponse, next?: Function): void;
route: string;
stack: ServerStackItem[];
/**
* Utilize the given middleware `handle` to the given `route`,
* defaulting to _/_. This "route" is the mount-point for the
* middleware, when given a value other than _/_ the middleware
* is only effective when that segment is present in the request's
* pathname.
*
* For example if we were to mount a function at _/admin_, it would
* be invoked on _/admin_, and _/admin/settings_, however it would
* not be invoked for _/_, or _/posts_.
*/
use(fn: NextHandleFunction): Server;
use(fn: HandleFunction): Server;
use(route: string, fn: NextHandleFunction): Server;
use(route: string, fn: HandleFunction): Server;
/**
* Handle server requests, punting them down
* the middleware stack.
*/
handle(req: http.IncomingMessage, res: http.ServerResponse, next: Function): void;
/**
* Listen for connections.
*
* This method takes the same arguments
* as node's `http.Server#listen()`.
*
* HTTP and HTTPS:
*
* If you run your application both as HTTP
* and HTTPS you may wrap them individually,
* since your Connect "server" is really just
* a JavaScript `Function`.
*
* var connect = require('connect')
* , http = require('http')
* , https = require('https');
*
* var app = connect();
*
* http.createServer(app).listen(80);
* https.createServer(options, app).listen(443);
*/
listen(port: number, hostname?: string, backlog?: number, callback?: Function): http.Server;
listen(port: number, hostname?: string, callback?: Function): http.Server;
listen(path: string, callback?: Function): http.Server;
listen(handle: any, listeningListener?: Function): http.Server;
}
}
export = createServer;

32
server/node_modules/@types/connect/package.json generated vendored Normal file
View File

@ -0,0 +1,32 @@
{
"name": "@types/connect",
"version": "3.4.38",
"description": "TypeScript definitions for connect",
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/connect",
"license": "MIT",
"contributors": [
{
"name": "Maxime LUCE",
"githubUsername": "SomaticIT",
"url": "https://github.com/SomaticIT"
},
{
"name": "Evan Hahn",
"githubUsername": "EvanHahn",
"url": "https://github.com/EvanHahn"
}
],
"main": "",
"types": "index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"directory": "types/connect"
},
"scripts": {},
"dependencies": {
"@types/node": "*"
},
"typesPublisherContentHash": "8990242237504bdec53088b79e314b94bec69286df9de56db31f22de403b4092",
"typeScriptVersion": "4.5"
}

21
server/node_modules/@types/cors/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

75
server/node_modules/@types/cors/README.md generated vendored Normal file
View File

@ -0,0 +1,75 @@
# Installation
> `npm install --save @types/cors`
# Summary
This package contains type definitions for cors (https://github.com/expressjs/cors/).
# Details
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/cors.
## [index.d.ts](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/cors/index.d.ts)
````ts
/// <reference types="node" />
import { IncomingHttpHeaders } from "http";
type StaticOrigin = boolean | string | RegExp | Array<boolean | string | RegExp>;
type CustomOrigin = (
requestOrigin: string | undefined,
callback: (err: Error | null, origin?: StaticOrigin) => void,
) => void;
declare namespace e {
interface CorsRequest {
method?: string | undefined;
headers: IncomingHttpHeaders;
}
interface CorsOptions {
/**
* @default '*'
*/
origin?: StaticOrigin | CustomOrigin | undefined;
/**
* @default 'GET,HEAD,PUT,PATCH,POST,DELETE'
*/
methods?: string | string[] | undefined;
allowedHeaders?: string | string[] | undefined;
exposedHeaders?: string | string[] | undefined;
credentials?: boolean | undefined;
maxAge?: number | undefined;
/**
* @default false
*/
preflightContinue?: boolean | undefined;
/**
* @default 204
*/
optionsSuccessStatus?: number | undefined;
}
type CorsOptionsDelegate<T extends CorsRequest = CorsRequest> = (
req: T,
callback: (err: Error | null, options?: CorsOptions) => void,
) => void;
}
declare function e<T extends e.CorsRequest = e.CorsRequest>(
options?: e.CorsOptions | e.CorsOptionsDelegate<T>,
): (
req: T,
res: {
statusCode?: number | undefined;
setHeader(key: string, value: string): any;
end(): any;
},
next: (err?: any) => any,
) => void;
export = e;
````
### Additional Details
* Last updated: Sat, 07 Jun 2025 02:15:25 GMT
* Dependencies: [@types/node](https://npmjs.com/package/@types/node)
# Credits
These definitions were written by [Alan Plum](https://github.com/pluma), [Gaurav Sharma](https://github.com/gtpan77), and [Sebastian Beltran](https://github.com/bjohansebas).

56
server/node_modules/@types/cors/index.d.ts generated vendored Normal file
View File

@ -0,0 +1,56 @@
/// <reference types="node" />
import { IncomingHttpHeaders } from "http";
type StaticOrigin = boolean | string | RegExp | Array<boolean | string | RegExp>;
type CustomOrigin = (
requestOrigin: string | undefined,
callback: (err: Error | null, origin?: StaticOrigin) => void,
) => void;
declare namespace e {
interface CorsRequest {
method?: string | undefined;
headers: IncomingHttpHeaders;
}
interface CorsOptions {
/**
* @default '*'
*/
origin?: StaticOrigin | CustomOrigin | undefined;
/**
* @default 'GET,HEAD,PUT,PATCH,POST,DELETE'
*/
methods?: string | string[] | undefined;
allowedHeaders?: string | string[] | undefined;
exposedHeaders?: string | string[] | undefined;
credentials?: boolean | undefined;
maxAge?: number | undefined;
/**
* @default false
*/
preflightContinue?: boolean | undefined;
/**
* @default 204
*/
optionsSuccessStatus?: number | undefined;
}
type CorsOptionsDelegate<T extends CorsRequest = CorsRequest> = (
req: T,
callback: (err: Error | null, options?: CorsOptions) => void,
) => void;
}
declare function e<T extends e.CorsRequest = e.CorsRequest>(
options?: e.CorsOptions | e.CorsOptionsDelegate<T>,
): (
req: T,
res: {
statusCode?: number | undefined;
setHeader(key: string, value: string): any;
end(): any;
},
next: (err?: any) => any,
) => void;
export = e;

Some files were not shown because too many files have changed in this diff Show More