Compare commits
5 Commits
21a6d57368
...
7ed44ac60e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed44ac60e | ||
|
|
af5b20d3e6 | ||
|
|
c678b60b69 | ||
|
|
9f45a9c43f | ||
|
|
0f6605a1ce |
201
README.md
|
|
@ -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
BIN
cloudflared-v2025.9.1-linux-amd64.tgz
Normal file
473
dist/assets/index-BailLCJ0.js
vendored
Normal file
1
dist/assets/index-HYQ6lVkA.css
vendored
Normal file
27
dist/callback.html
vendored
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDMwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIiBmaWxsPSIjMjgyODI4Ii8+CjxjaXJjbGUgY3g9IjE1MCIgY3k9IjE1MCIgcj0iNTAiIGZpbGw9IiMxZGI5NTQiLz4KPHN2ZyB4PSIxMjUiIHk9IjEyNSIgd2lkdGg9IjUwIiBoZWlnaHQ9IjUwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiPgo8cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJTNi40OCAyMiAxMiAyMlMyMiAxNy41MiAyMiAxMlMxNy41MiAyIDEyIDJaTTEwIDE3TDUgMTJMMTAgN0wxMSAxMkwxNyAxMkwxMCAxN1oiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo8L3N2Zz4K
|
||||
7
env.example
Normal 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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
589178
|
||||
14
index.html
Normal 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
|
|
@ -1,7 +0,0 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
8383
package-lock.json
generated
63
package.json
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
2
preview-3443.out
Normal 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)
|
||||
|
|
@ -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")
|
||||
}
|
||||
127
production-server.js
Normal 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
|
|
@ -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>
|
||||
|
|
@ -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 |
|
|
@ -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
|
|
@ -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 |
|
|
@ -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 |
1
public/placeholder-album.png
Normal file
|
|
@ -0,0 +1 @@
|
|||
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDMwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIiBmaWxsPSIjMjgyODI4Ii8+CjxjaXJjbGUgY3g9IjE1MCIgY3k9IjE1MCIgcj0iNTAiIGZpbGw9IiMxZGI5NTQiLz4KPHN2ZyB4PSIxMjUiIHk9IjEyNSIgd2lkdGg9IjUwIiBoZWlnaHQ9IjUwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiPgo8cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJTNi40OCAyMiAxMiAyMlMyMiAxNy41MiAyMiAxMlMxNy41MiAyIDEyIDJaTTEwIDE3TDUgMTJMMTAgN0wxMSAxMkwxNyAxMkwxMCAxN1oiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo8L3N2Zz4K
|
||||
|
|
@ -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 |
|
|
@ -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
|
|
@ -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
|
||||
[0mPOST /auth/exchange [32m200[0m 144.220 ms - 749[0m
|
||||
[0mPOST /users/91416peucsefexd7z3q9875hw/sync [32m200[0m 349.388 ms - 11[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 12.725 ms - 156666[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw [32m200[0m 1.174 ms - 232[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/top-tracks?time_range=short_term [32m200[0m 4.523 ms - 73042[0m
|
||||
[0mGET /partners/partner/91416peucsefexd7z3q9875hw [32m200[0m 3.006 ms - 44[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy [32m200[0m 0.816 ms - 238[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 7.860 ms - 156852[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term [32m200[0m 3.907 ms - 61623[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 9.232 ms - 156666[0m
|
||||
[0mGET /partners/requests/91416peucsefexd7z3q9875hw [32m200[0m 11.611 ms - 2[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 1.466 ms - 57[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 0.873 ms - 57[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 8.504 ms - 156852[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 35.615 ms - 20[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/now-playing [32m200[0m 3.749 ms - 20[0m
|
||||
[0mGET /playlists/mixed [32m200[0m 2.786 ms - 5029[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg [32m200[0m 3.610 ms - 264761[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg [32m200[0m 2.557 ms - 144313[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg [32m200[0m 2.834 ms - 204401[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg [32m200[0m 5.850 ms - 264761[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg [32m200[0m 3.053 ms - 144313[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg [32m200[0m 3.028 ms - 204401[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg [32m200[0m 6.302 ms - 264761[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg [32m200[0m 9.533 ms - 144313[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg [32m200[0m 12.364 ms - 204401[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg [32m200[0m 5.427 ms - 264761[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg [32m200[0m 9.141 ms - 204401[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg [32m200[0m 2.173 ms - 144313[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg [32m200[0m 3.012 ms - 264761[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg [32m200[0m 3.370 ms - 144313[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg [32m200[0m 2.712 ms - 204401[0m
|
||||
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'
|
||||
]
|
||||
}
|
||||
[0mPOST /playlists/mixed [32m200[0m 1772.528 ms - 1400[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg [32m200[0m 6.630 ms - 264761[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg [32m200[0m 11.088 ms - 204401[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 12.794 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg [32m200[0m 6.741 ms - 144313[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg [32m200[0m 3.646 ms - 204401[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 3.497 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg [32m200[0m 6.103 ms - 144313[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29.jpeg [32m200[0m 4.945 ms - 264761[0m
|
||||
[0mDELETE /playlists/mixed/a8580fac-781a-472e-94ce-ac88e7f67cb4 [32m200[0m 2.750 ms - 16[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg [32m200[0m 3.500 ms - 204401[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 2.855 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(4).jpeg [32m200[0m 5.074 ms - 144313[0m
|
||||
[0mDELETE /playlists/mixed/ac740cb2-3d6b-4a03-94d1-7c5bbb4514fa [32m200[0m 2.122 ms - 16[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 3.704 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(5).jpeg [32m200[0m 6.880 ms - 204401[0m
|
||||
[0mDELETE /playlists/mixed/21f99970-416e-474a-8819-aaab31afa649 [32m200[0m 2.111 ms - 16[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 2.857 ms - 132357[0m
|
||||
[0mDELETE /playlists/mixed/008255d7-98e8-493d-8bfd-659b0bdcbd09 [32m200[0m 1.747 ms - 16[0m
|
||||
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
|
||||
[0mGET / [33m404[0m 4.074 ms - 139[0m
|
||||
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
|
||||
[0mGET /partners/partner/91416peucsefexd7z3q9875hw [32m200[0m 10.347 ms - 44[0m
|
||||
[0mGET /playlists/mixed [32m200[0m 3.149 ms - 16[0m
|
||||
[0mGET /partners/partner/91416peucsefexd7z3q9875hw [32m200[0m 2.377 ms - 44[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 7.331 ms - 156666[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 5.987 ms - 156852[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term [32m200[0m 3.810 ms - 61623[0m
|
||||
[0mGET /partners/requests/91416peucsefexd7z3q9875hw [32m200[0m 2.445 ms - 2[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 1.295 ms - 57[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy [32m200[0m 2.151 ms - 238[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 7.602 ms - 156852[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term [32m200[0m 2.906 ms - 61623[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 98.682 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 7.454 ms - 57[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy [32m200[0m 1.046 ms - 238[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 5.938 ms - 156666[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 7.101 ms - 156852[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 44.738 ms - 20[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/now-playing [32m200[0m 2.759 ms - 20[0m
|
||||
[0mGET /playlists/mixed [32m200[0m 2.235 ms - 16[0m
|
||||
[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'
|
||||
}
|
||||
[0mPOST /playlists/mixed [32m200[0m 1429.828 ms - 1359[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 3.533 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.969 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.872 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 1.197 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.691 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.926 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 1.094 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.750 ms - 736[0m
|
||||
[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
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.638 ms - 736[0m
|
||||
[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'
|
||||
}
|
||||
[0mPOST /playlists/mixed [32m200[0m 1239.488 ms - 1356[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.613 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.759 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.689 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.667 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.969 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.757 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.999 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.927 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.763 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.873 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.677 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.809 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.867 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.766 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.803 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.678 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.800 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.764 ms - 736[0m
|
||||
[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
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.874 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.756 ms - 736[0m
|
||||
[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'
|
||||
}
|
||||
[0mPOST /playlists/mixed [32m200[0m 1497.571 ms - 1395[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 1.352 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 5.426 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.694 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.116 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.757 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [32m200[0m 0.685 ms - 736[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.725 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.131 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.736 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.627 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.331 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.537 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.683 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 1.895 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.571 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 1.176 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.786 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.588 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 1.487 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 3.403 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.698 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.659 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 3.457 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.695 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 1.205 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 3.829 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.572 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.953 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.118 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.782 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 1.583 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 4.709 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.857 ms - 27[0m
|
||||
[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
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 1.120 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 3.859 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 1.464 ms - 27[0m
|
||||
[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'
|
||||
}
|
||||
[0mPOST /playlists/mixed [32m200[0m 1488.677 ms - 1400[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.880 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 3.061 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 1.993 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.668 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 1.960 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.936 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 1.543 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.644 ms - 27[0m
|
||||
[0mGET /partners/partner/91416peucsefexd7z3q9875hw [32m200[0m 2.126 ms - 44[0m
|
||||
[0mGET /playlists/mixed [32m200[0m 2.656 ms - 6620[0m
|
||||
[0mGET /partners/partner/91416peucsefexd7z3q9875hw [32m200[0m 2.145 ms - 44[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 5.171 ms - 156666[0m
|
||||
[0mGET /partners/requests/91416peucsefexd7z3q9875hw [32m200[0m 3.568 ms - 2[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 0.821 ms - 57[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term [32m200[0m 2.420 ms - 61623[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 4.760 ms - 156852[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy [32m200[0m 0.684 ms - 238[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 0.511 ms - 57[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 4.954 ms - 156852[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term [32m200[0m 4.000 ms - 61623[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy [32m200[0m 4.409 ms - 238[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 6.305 ms - 156666[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 4.941 ms - 156852[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 331.408 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 386.557 ms - 20[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/now-playing [32m200[0m 2.108 ms - 20[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 4.406 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 6.255 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 6.573 ms - 27[0m
|
||||
[0mGET /playlists/mixed [32m200[0m 2.180 ms - 6620[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.499 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 3.759 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 5.424 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.527 ms - 27[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 0.985 ms - 27[0m
|
||||
[0mDELETE /playlists/mixed/8ec2d0d9-de93-4ee7-ab76-69dc6ea6b432 [32m200[0m 1.857 ms - 16[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 3.592 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.204 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/cover1.svg [33m404[0m 2.529 ms - 27[0m
|
||||
[0mDELETE /playlists/mixed/f9d6bf45-4b8f-472a-b482-e66fd88a3e16 [32m200[0m 1.599 ms - 16[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 2.167 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 5.011 ms - 131729[0m
|
||||
[0mGET /partners/partner/91416peucsefexd7z3q9875hw [32m200[0m 1.746 ms - 44[0m
|
||||
[0mGET /playlists/mixed [32m200[0m 2.243 ms - 3359[0m
|
||||
[0mGET /partners/partner/91416peucsefexd7z3q9875hw [32m200[0m 3.642 ms - 44[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 7.241 ms - 156666[0m
|
||||
[0mGET /partners/requests/91416peucsefexd7z3q9875hw [32m200[0m 1.888 ms - 2[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 0.669 ms - 57[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy [32m200[0m 0.909 ms - 238[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 6.056 ms - 156852[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term [32m200[0m 2.810 ms - 61623[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 53.602 ms - 20[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy [32m200[0m 0.725 ms - 238[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term [32m200[0m 2.653 ms - 61623[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 6.048 ms - 156852[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 0.730 ms - 57[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 5.416 ms - 156666[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 4.647 ms - 156852[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 44.441 ms - 20[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/now-playing [32m200[0m 2.167 ms - 20[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 2.594 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 4.424 ms - 131729[0m
|
||||
[0mGET /playlists/mixed [32m200[0m 5.963 ms - 3359[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 2.812 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 1.637 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 1.989 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.290 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 3.188 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.014 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 3.943 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 6.145 ms - 131729[0m
|
||||
[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
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.525 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 2.561 ms - 132357[0m
|
||||
[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'
|
||||
}
|
||||
[0mPOST /playlists/mixed [32m200[0m 1309.712 ms - 1402[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 4.178 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 6.168 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg [32m200[0m 4.953 ms - 270893[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 2.447 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 1.672 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg [32m200[0m 4.397 ms - 270893[0m
|
||||
[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
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.961 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 6.393 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg [32m200[0m 9.415 ms - 270893[0m
|
||||
[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'
|
||||
}
|
||||
[0mPOST /playlists/mixed [32m200[0m 1604.637 ms - 1402[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 2.817 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 4.009 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg [32m200[0m 3.588 ms - 270893[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg [32m200[0m 6.224 ms - 133957[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 1.734 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.503 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg [32m200[0m 3.109 ms - 270893[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg [32m200[0m 2.680 ms - 133957[0m
|
||||
[0mGET /partners/events/91416peucsefexd7z3q9875hw?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI5MTQxNnBldWNzZWZleGQ3ejNxOTg3NWh3IiwiaWF0IjoxNzYwNjE2MDA3LCJleHAiOjE3NjMyMDgwMDd9.E5NRRqWPxfauImmWE0zi1eQlckg6cPN1i-ojwyXnYuI [32m200[0m 1.312 ms - -[0m
|
||||
[0mGET /partners/events/91416peucsefexd7z3q9875hw?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI5MTQxNnBldWNzZWZleGQ3ejNxOTg3NWh3IiwiaWF0IjoxNzYwNjE2MDA3LCJleHAiOjE3NjMyMDgwMDd9.E5NRRqWPxfauImmWE0zi1eQlckg6cPN1i-ojwyXnYuI [32m200[0m 2.337 ms - -[0m
|
||||
[0mGET /partners/events/91416peucsefexd7z3q9875hw?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI5MTQxNnBldWNzZWZleGQ3ejNxOTg3NWh3IiwiaWF0IjoxNzYwNjE2MDA3LCJleHAiOjE3NjMyMDgwMDd9.E5NRRqWPxfauImmWE0zi1eQlckg6cPN1i-ojwyXnYuI [32m200[0m 1.647 ms - -[0m
|
||||
[0mGET /health [32m200[0m 0.661 ms - 11[0m
|
||||
[0mGET /partners/partner/91416peucsefexd7z3q9875hw [32m200[0m 1.623 ms - 44[0m
|
||||
[0mGET /playlists/mixed [32m200[0m 2.029 ms - 6709[0m
|
||||
[0mGET /partners/partner/91416peucsefexd7z3q9875hw [32m200[0m 1.555 ms - 44[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 5.543 ms - 156666[0m
|
||||
[0mGET /partners/requests/91416peucsefexd7z3q9875hw [32m200[0m 1.618 ms - 2[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 0.735 ms - 57[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 4.746 ms - 156852[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy [32m200[0m 0.913 ms - 238[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term [32m200[0m 2.492 ms - 61623[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy [32m200[0m 2.921 ms - 238[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 5.500 ms - 156852[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 34.610 ms - 20[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/top-tracks?time_range=short_term [32m200[0m 3.567 ms - 61623[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/status [32m200[0m 0.847 ms - 57[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/recently-played [32m200[0m 6.017 ms - 156666[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 32.753 ms - 20[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/now-playing [32m200[0m 2.014 ms - 20[0m
|
||||
[0mGET /users/31at7552nbs34vogjfzmasypv6vy/recently-played [32m200[0m 5.002 ms - 156852[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg [32m200[0m 3.188 ms - 133957[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg [32m200[0m 6.106 ms - 270893[0m
|
||||
[0mGET /playlists/mixed [32m200[0m 2.215 ms - 6709[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 4.627 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 1.706 ms - 131729[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg [32m200[0m 3.480 ms - 133957[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg [32m200[0m 3.911 ms - 270893[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 5.660 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.253 ms - 131729[0m
|
||||
[0mDELETE /playlists/mixed/bc5de7f1-f91c-45e8-9549-ff411e05f47d [32m200[0m 1.912 ms - 16[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30%20(2).jpeg [32m200[0m 4.191 ms - 270893[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 3.256 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 4.664 ms - 131729[0m
|
||||
[0mDELETE /playlists/mixed/8bba02a1-389e-46e8-b4a2-efbb016647a3 [32m200[0m 1.718 ms - 16[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(3).jpeg [32m200[0m 3.657 ms - 132357[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 5.127 ms - 131729[0m
|
||||
[0mDELETE /playlists/mixed/f66b57ab-0baa-4740-9303-fedbc5b90df6 [32m200[0m 1.900 ms - 16[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.30.jpeg [32m200[0m 2.258 ms - 131729[0m
|
||||
[0mDELETE /playlists/mixed/8791c148-a234-4e08-8ff2-04f54f4348a1 [32m200[0m 2.063 ms - 16[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 88.599 ms - 20[0m
|
||||
[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'
|
||||
}
|
||||
[0mPOST /playlists/mixed [32m200[0m 2897.898 ms - 1401[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg [32m200[0m 2.457 ms - 133957[0m
|
||||
[0mGET /api/playlist-covers/WhatsApp%20Image%202025-10-16%20at%2011.12.29%20(1).jpeg [32m200[0m 2.064 ms - 133957[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 44.537 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 85.326 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 83.071 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 63.326 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 83.462 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 48.190 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 86.442 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 55.034 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 74.207 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 88.228 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 34.562 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 89.815 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 51.537 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 52.542 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 59.649 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 50.169 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 87.490 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 85.943 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 55.879 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 56.865 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 54.869 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 85.565 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 53.059 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 54.127 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 98.826 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 87.836 ms - 20[0m
|
||||
[0mGET /users/91416peucsefexd7z3q9875hw/now-playing [32m200[0m 91.096 ms - 20[0m
|
||||
1
server-dev.pid
Normal file
|
|
@ -0,0 +1 @@
|
|||
619472
|
||||
26
server.log
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
../esbuild/bin/esbuild
|
||||
1
server/node_modules/.bin/mime
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../mime/cli.js
|
||||
1
server/node_modules/.bin/prebuild-install
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../prebuild-install/bin.js
|
||||
1
server/node_modules/.bin/rc
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../rc/cli.js
|
||||
1
server/node_modules/.bin/semver
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../semver/bin/semver.js
|
||||
1
server/node_modules/.bin/tsc
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../typescript/bin/tsc
|
||||
1
server/node_modules/.bin/tsserver
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../typescript/bin/tsserver
|
||||
1
server/node_modules/.bin/tsx
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../tsx/dist/cli.mjs
|
||||
1
server/node_modules/.bin/uuid
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../uuid/dist-node/bin/uuid
|
||||
1996
server/node_modules/.package-lock.json
generated
vendored
Normal file
3
server/node_modules/@esbuild/linux-x64/README.md
generated
vendored
Normal 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
20
server/node_modules/@esbuild/linux-x64/package.json
generated
vendored
Normal 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
|
|
@ -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
|
|
@ -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
1
server/node_modules/@img/colour/index.cjs
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = require("./color.cjs").default;
|
||||
45
server/node_modules/@img/colour/package.json
generated
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
46
server/node_modules/@img/sharp-libvips-linux-x64/README.md
generated
vendored
Normal 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
|
||||
221
server/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h
generated
vendored
Normal 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__ */
|
||||
1
server/node_modules/@img/sharp-libvips-linux-x64/lib/index.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = __dirname;
|
||||
BIN
server/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.2
generated
vendored
Normal file
42
server/node_modules/@img/sharp-libvips-linux-x64/package.json
generated
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
30
server/node_modules/@img/sharp-libvips-linux-x64/versions.json
generated
vendored
Normal 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"
|
||||
}
|
||||
46
server/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md
generated
vendored
Normal 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
|
||||
221
server/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h
generated
vendored
Normal 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__ */
|
||||
1
server/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = __dirname;
|
||||
BIN
server/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.2
generated
vendored
Normal file
42
server/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json
generated
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
30
server/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json
generated
vendored
Normal 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
|
|
@ -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
|
|
@ -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.
|
||||
BIN
server/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node
generated
vendored
Normal file
46
server/node_modules/@img/sharp-linux-x64/package.json
generated
vendored
Normal 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
|
|
@ -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
|
|
@ -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.
|
||||
BIN
server/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node
generated
vendored
Normal file
46
server/node_modules/@img/sharp-linuxmusl-x64/package.json
generated
vendored
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||