psql
This commit is contained in:
26
.serena/memories/postgresql_migration_complete.md
Normal file
26
.serena/memories/postgresql_migration_complete.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# PostgreSQL Migration Completed
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
- **Docker Compose**: Added PostgreSQL 15 service with persistent data volume
|
||||||
|
- **Database Configuration**: Migrated from SQLite3 to PostgreSQL using pg library
|
||||||
|
- **Package Dependencies**: Updated package.json (removed sqlite3, added pg and @types/pg)
|
||||||
|
- **Database Class**: Converted all methods to use PostgreSQL syntax and async/await pattern
|
||||||
|
|
||||||
|
## Key Configuration
|
||||||
|
- Database URL: `postgresql://quixotic:quixotic123@postgres:5432/quixotic`
|
||||||
|
- Container: `quixotic-postgres` with health checks
|
||||||
|
- Volume: `postgres-data` for data persistence
|
||||||
|
- Environment variables: POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD
|
||||||
|
|
||||||
|
## Database Schema Changes
|
||||||
|
- INTEGER PRIMARY KEY → SERIAL PRIMARY KEY
|
||||||
|
- BIGINT for telegram_id (better for large Telegram IDs)
|
||||||
|
- TIMESTAMP instead of DATETIME
|
||||||
|
- PostgreSQL-style REFERENCES syntax for foreign keys
|
||||||
|
- ON CONFLICT DO UPDATE for upsert operations
|
||||||
|
|
||||||
|
## Status
|
||||||
|
✅ Migration completed successfully
|
||||||
|
✅ Application starts without database errors
|
||||||
|
✅ All dependencies installed
|
||||||
|
✅ Docker containers running properly
|
||||||
34
README.md
34
README.md
@@ -1,36 +1,6 @@
|
|||||||
# 🎵 Quixotic - Music Search Telegram MiniApp
|
# 🎵 Quixotic - Music Search Telegram MiniApp
|
||||||
|
|
||||||
Telegram miniapp для поиска и скачивания музыки (в разработке).
|
Telegram miniapp для поиска и скачивания музыки
|
||||||
|
|
||||||
## ⚠️ Current Status: UNDER DEVELOPMENT
|
|
||||||
|
|
||||||
**The app is currently non-functional** due to the following issues:
|
|
||||||
|
|
||||||
- ❌ **YouTube integration abandoned** - Bot detection blocks all anonymous access
|
|
||||||
- ❌ **SoundCloud integration failed** - API restrictions and ID mismatch between frontend/backend
|
|
||||||
- 🔄 **TypeScript migration completed** - All code converted from JavaScript to TypeScript
|
|
||||||
- 🔄 **Alternative sources being evaluated** - Archive.org, Jamendo, Bandcamp under consideration
|
|
||||||
|
|
||||||
### What Works:
|
|
||||||
- ✅ Telegram Bot setup and Web App integration
|
|
||||||
- ✅ Frontend interface (search/UI)
|
|
||||||
- ✅ SQLite database functionality
|
|
||||||
- ✅ Docker deployment setup with Traefik SSL
|
|
||||||
- ✅ Full TypeScript support with proper typing
|
|
||||||
|
|
||||||
### What's Broken:
|
|
||||||
- ❌ Music search (no working backend service)
|
|
||||||
- ❌ MP3 conversion (depends on working search)
|
|
||||||
- ❌ Download functionality
|
|
||||||
|
|
||||||
## Планируемые возможности
|
|
||||||
|
|
||||||
- 🔍 Поиск музыки (источник определяется)
|
|
||||||
- 🎵 Конвертация в MP3 с помощью FFmpeg
|
|
||||||
- 📱 Telegram Web App интерфейс (✅ готов)
|
|
||||||
- 💾 SQLite база данных (✅ готова)
|
|
||||||
- 📊 История поиска (✅ готова)
|
|
||||||
- 🤖 Telegram Bot интеграция (✅ готова)
|
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
@@ -94,8 +64,6 @@ PORT=3000
|
|||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
⚠️ **Внимание**: Приложение сейчас не работает из-за проблем с интеграцией музыкальных сервисов
|
|
||||||
|
|
||||||
### Разработка
|
### Разработка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -32,6 +32,26 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- quixotic
|
- quixotic
|
||||||
|
|
||||||
|
# PostgreSQL database
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: quixotic-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-quixotic}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-quixotic}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-quixotic123}
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
- ./database/init:/docker-entrypoint-initdb.d
|
||||||
|
networks:
|
||||||
|
- quixotic
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-quixotic}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
# Main application
|
# Main application
|
||||||
quixotic-app:
|
quixotic-app:
|
||||||
build:
|
build:
|
||||||
@@ -42,9 +62,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
|
- DATABASE_URL=postgresql://${POSTGRES_USER:-quixotic}:${POSTGRES_PASSWORD:-quixotic123}@postgres:5432/${POSTGRES_DB:-quixotic}
|
||||||
volumes:
|
volumes:
|
||||||
- downloads:/app/downloads
|
- downloads:/app/downloads
|
||||||
- ./database:/app/database
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.quixotic.rule=Host(`${DOMAIN:-localhost}`)"
|
- "traefik.http.routers.quixotic.rule=Host(`${DOMAIN:-localhost}`)"
|
||||||
@@ -59,12 +79,14 @@ services:
|
|||||||
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
|
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
|
||||||
depends_on:
|
depends_on:
|
||||||
- traefik
|
- traefik
|
||||||
|
- postgres
|
||||||
networks:
|
networks:
|
||||||
- quixotic
|
- quixotic
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
traefik-ssl-certs:
|
traefik-ssl-certs:
|
||||||
downloads:
|
downloads:
|
||||||
|
postgres-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
quixotic:
|
quixotic:
|
||||||
|
|||||||
@@ -31,12 +31,13 @@
|
|||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"node-telegram-bot-api": "^0.64.0",
|
"node-telegram-bot-api": "^0.64.0",
|
||||||
"soundcloud-downloader": "^1.0.0",
|
"soundcloud-downloader": "^1.0.0",
|
||||||
"sqlite3": "^5.1.6"
|
"pg": "^8.11.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/fluent-ffmpeg": "^2.1.27",
|
"@types/fluent-ffmpeg": "^2.1.27",
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
|
"@types/pg": "^8.10.9",
|
||||||
"@types/node-telegram-bot-api": "^0.64.10",
|
"@types/node-telegram-bot-api": "^0.64.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||||
"@typescript-eslint/parser": "^8.41.0",
|
"@typescript-eslint/parser": "^8.41.0",
|
||||||
|
|||||||
128
src/database.ts
128
src/database.ts
@@ -1,5 +1,4 @@
|
|||||||
import sqlite3 from 'sqlite3';
|
import { Pool, QueryResult } from 'pg';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
interface TelegramUser {
|
interface TelegramUser {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -18,104 +17,91 @@ interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
private dbPath: string;
|
private pool: Pool;
|
||||||
private db: sqlite3.Database;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dbPath = path.join(__dirname, '../database/quixotic.db');
|
const connectionString = process.env.DATABASE_URL || 'postgresql://quixotic:quixotic123@localhost:5432/quixotic';
|
||||||
this.db = new sqlite3.Database(this.dbPath);
|
this.pool = new Pool({
|
||||||
|
connectionString,
|
||||||
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
|
});
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(): void {
|
private async init(): Promise<void> {
|
||||||
this.db.serialize(() => {
|
try {
|
||||||
// Users table
|
// Users table
|
||||||
this.db.run(`CREATE TABLE IF NOT EXISTS users (
|
await this.pool.query(`CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
telegram_id INTEGER UNIQUE NOT NULL,
|
telegram_id BIGINT UNIQUE NOT NULL,
|
||||||
username TEXT,
|
username TEXT,
|
||||||
first_name TEXT,
|
first_name TEXT,
|
||||||
last_name TEXT,
|
last_name TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)`);
|
)`);
|
||||||
|
|
||||||
// Search history table
|
// Search history table
|
||||||
this.db.run(`CREATE TABLE IF NOT EXISTS search_history (
|
await this.pool.query(`CREATE TABLE IF NOT EXISTS search_history (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER,
|
user_id INTEGER REFERENCES users(id),
|
||||||
query TEXT NOT NULL,
|
query TEXT NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
||||||
)`);
|
)`);
|
||||||
|
|
||||||
// Downloaded files table
|
// Downloaded files table
|
||||||
this.db.run(`CREATE TABLE IF NOT EXISTS downloads (
|
await this.pool.query(`CREATE TABLE IF NOT EXISTS downloads (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER,
|
user_id INTEGER REFERENCES users(id),
|
||||||
youtube_id TEXT NOT NULL,
|
youtube_id TEXT NOT NULL,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
||||||
)`);
|
)`);
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error('Database initialization error:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addUser(telegramUser: TelegramUser): Promise<number> {
|
async addUser(telegramUser: TelegramUser): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
const { id, username, first_name, last_name } = telegramUser;
|
||||||
const { id, username, first_name, last_name } = telegramUser;
|
const result = await this.pool.query(
|
||||||
this.db.run(
|
`INSERT INTO users (telegram_id, username, first_name, last_name)
|
||||||
`INSERT OR REPLACE INTO users (telegram_id, username, first_name, last_name)
|
VALUES ($1, $2, $3, $4)
|
||||||
VALUES (?, ?, ?, ?)`,
|
ON CONFLICT (telegram_id) DO UPDATE SET
|
||||||
[id, username, first_name, last_name],
|
username = EXCLUDED.username,
|
||||||
function(err: Error | null) {
|
first_name = EXCLUDED.first_name,
|
||||||
if (err) reject(err);
|
last_name = EXCLUDED.last_name
|
||||||
else resolve(this.lastID);
|
RETURNING id`,
|
||||||
}
|
[id, username, first_name, last_name]
|
||||||
);
|
);
|
||||||
});
|
return result.rows[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
addSearchHistory(userId: number, query: string): Promise<number> {
|
async addSearchHistory(userId: number, query: string): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
const result = await this.pool.query(
|
||||||
this.db.run(
|
'INSERT INTO search_history (user_id, query) VALUES ($1, $2) RETURNING id',
|
||||||
'INSERT INTO search_history (user_id, query) VALUES (?, ?)',
|
[userId, query]
|
||||||
[userId, query],
|
);
|
||||||
function(err: Error | null) {
|
return result.rows[0].id;
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(this.lastID);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addDownload(userId: number, youtubeId: string, title: string, filePath: string): Promise<number> {
|
async addDownload(userId: number, youtubeId: string, title: string, filePath: string): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
const result = await this.pool.query(
|
||||||
this.db.run(
|
'INSERT INTO downloads (user_id, youtube_id, title, file_path) VALUES ($1, $2, $3, $4) RETURNING id',
|
||||||
'INSERT INTO downloads (user_id, youtube_id, title, file_path) VALUES (?, ?, ?, ?)',
|
[userId, youtubeId, title, filePath]
|
||||||
[userId, youtubeId, title, filePath],
|
);
|
||||||
function(err: Error | null) {
|
return result.rows[0].id;
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(this.lastID);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserByTelegramId(telegramId: string | number): Promise<User | undefined> {
|
async getUserByTelegramId(telegramId: string | number): Promise<User | undefined> {
|
||||||
return new Promise((resolve, reject) => {
|
const result = await this.pool.query(
|
||||||
this.db.get(
|
'SELECT * FROM users WHERE telegram_id = $1',
|
||||||
'SELECT * FROM users WHERE telegram_id = ?',
|
[telegramId]
|
||||||
[telegramId],
|
);
|
||||||
(err: Error | null, row: User) => {
|
return result.rows[0] || undefined;
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(row);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): void {
|
async close(): Promise<void> {
|
||||||
this.db.close();
|
await this.pool.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user