kinopoisk-movie-api / database.js
opex792's picture
Upload database.js
c05d5c9 verified
raw
history blame
13.2 kB
import { open } from 'sqlite';
import sqlite3 from 'sqlite3';
import fetch from 'node-fetch';
import { createGunzip } from 'zlib';
import { createInterface } from 'readline';
const DB_FILE = './kinopoisk_main.db';
const DATA_URL = 'https://huggingface.co/datasets/opex792/kinopoisk/resolve/main/consolidated/kinopoisk.jsonl.gz?download=true';
let db;
/**
* Инициализирует базу данных. Схема остается прежней.
*/
export async function initializeDatabase() {
db = await open({ filename: DB_FILE, driver: sqlite3.Database });
console.log('Подключение к SQLite (Main API) установлено.');
await db.exec('PRAGMA journal_mode = WAL;');
await db.exec('PRAGMA synchronous = NORMAL;');
await db.exec(`
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
year INTEGER,
type TEXT,
rating_kp REAL,
rating_imdb REAL,
age_rating INTEGER,
movie_length INTEGER,
is_series BOOLEAN,
data TEXT
);
CREATE TABLE IF NOT EXISTS genres (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);
CREATE TABLE IF NOT EXISTS countries (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE);
CREATE TABLE IF NOT EXISTS movie_genres (movie_id INTEGER, genre_id INTEGER, FOREIGN KEY(movie_id) REFERENCES movies(id), FOREIGN KEY(genre_id) REFERENCES genres(id), PRIMARY KEY (movie_id, genre_id));
CREATE TABLE IF NOT EXISTS movie_countries (movie_id INTEGER, country_id INTEGER, FOREIGN KEY(movie_id) REFERENCES movies(id), FOREIGN KEY(country_id) REFERENCES countries(id), PRIMARY KEY (movie_id, country_id));
CREATE INDEX IF NOT EXISTS idx_year ON movies(year);
CREATE INDEX IF NOT EXISTS idx_rating_kp ON movies(rating_kp);
CREATE INDEX IF NOT EXISTS idx_type ON movies(type);
`);
const count = await db.get('SELECT COUNT(id) as count FROM movies');
if (count.count === 0) {
console.log('База данных пуста. Запускаю начальную загрузку...');
await refreshData();
} else {
console.log(`В базе уже есть ${count.count} записей.`);
}
return db;
}
/**
* Переработанная функция загрузки данных, использующая потоковую обработку
* для предотвращения переполнения памяти.
*/
export async function refreshData() {
console.log('Начинается высокопроизводительная перезагрузка данных...');
try {
// --- ШАГ 1: Сканирование файла для сбора метаданных (жанры, страны) ---
console.log('Шаг 1: Сканирование файла для сбора жанров и стран...');
const response1 = await fetch(DATA_URL);
if (!response1.ok) throw new Error(`Ошибка загрузки на первом проходе: ${response1.statusText}`);
const gunzip1 = createGunzip();
response1.body.pipe(gunzip1);
const rl1 = createInterface({ input: gunzip1, crlfDelay: Infinity });
const allGenres = new Set();
const allCountries = new Set();
for await (const line of rl1) {
if (line.trim()) {
// Мы не храним весь объект, только извлекаем нужные данные
const movie = JSON.parse(line);
movie.genres?.forEach(g => allGenres.add(g.name));
movie.countries?.forEach(c => allCountries.add(c.name));
}
}
console.log(`Шаг 1 завершен. Найдено ${allGenres.size} уникальных жанров, ${allCountries.size} уникальных стран.`);
// --- Начало транзакции для массовой вставки ---
await db.exec('BEGIN TRANSACTION;');
// Очищаем таблицы перед новой загрузкой
await db.exec('DELETE FROM movie_genres;');
await db.exec('DELETE FROM movie_countries;');
await db.exec('DELETE FROM genres;');
await db.exec('DELETE FROM countries;');
await db.exec('DELETE FROM movies;');
// --- ШАГ 2: Пакетная вставка жанров и стран ---
console.log('Шаг 2: Пакетная запись жанров и стран...');
const genreStmt = await db.prepare('INSERT INTO genres (name) VALUES (?)');
for (const name of allGenres) {
await genreStmt.run(name);
}
await genreStmt.finalize();
const countryStmt = await db.prepare('INSERT INTO countries (name) VALUES (?)');
for (const name of allCountries) {
await countryStmt.run(name);
}
await countryStmt.finalize();
console.log('Шаг 2 завершен.');
// --- ШАГ 3: Кэширование ID жанров и стран для быстрой связи ---
console.log('Шаг 3: Кэширование ID в память...');
const genreCache = new Map();
const countryCache = new Map();
const genresFromDb = await db.all('SELECT id, name FROM genres');
genresFromDb.forEach(g => genreCache.set(g.name, g.id));
const countriesFromDb = await db.all('SELECT id, name FROM countries');
countriesFromDb.forEach(c => countryCache.set(c.name, c.id));
console.log('Шаг 3 завершен.');
// --- ШАГ 4: Потоковая вставка фильмов и их связей (второй проход) ---
console.log('Шаг 4: Потоковая запись фильмов и их связей...');
const response2 = await fetch(DATA_URL);
if (!response2.ok) throw new Error(`Ошибка загрузки на втором проходе: ${response2.statusText}`);
const gunzip2 = createGunzip();
response2.body.pipe(gunzip2);
const rl2 = createInterface({ input: gunzip2, crlfDelay: Infinity });
const movieStmt = await db.prepare('INSERT INTO movies (id, year, type, rating_kp, rating_imdb, age_rating, movie_length, is_series, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
const movieGenreStmt = await db.prepare('INSERT OR IGNORE INTO movie_genres (movie_id, genre_id) VALUES (?, ?)');
const movieCountryStmt = await db.prepare('INSERT OR IGNORE INTO movie_countries (movie_id, country_id) VALUES (?, ?)');
let processedCount = 0;
for await (const line of rl2) {
if (line.trim()) {
const movie = JSON.parse(line);
if (!movie.id) continue;
await movieStmt.run(movie.id, movie.year, movie.type, movie.rating?.kp, movie.rating?.imdb, movie.ageRating, movie.movieLength, movie.isSeries, JSON.stringify(movie));
if (movie.genres) {
for (const genre of movie.genres) {
const genreId = genreCache.get(genre.name);
if (genreId) await movieGenreStmt.run(movie.id, genreId);
}
}
if (movie.countries) {
for (const country of movie.countries) {
const countryId = countryCache.get(country.name);
if (countryId) await movieCountryStmt.run(movie.id, countryId);
}
}
processedCount++;
if (processedCount % 10000 === 0) {
console.log(`Обработано ${processedCount} фильмов...`);
}
}
}
await movieStmt.finalize();
await movieGenreStmt.finalize();
await movieCountryStmt.finalize();
await db.exec('COMMIT;');
console.log(`Шаг 4 завершен. Перезагрузка данных успешно окончена. Всего обработано ${processedCount} фильмов.`);
} catch (error) {
console.error('Критическая ошибка во время обновления данных:', error);
try {
await db.exec('ROLLBACK;');
console.log('Транзакция была отменена.');
} catch (rollbackError) {
console.error('Ошибка при откате транзакции:', rollbackError);
}
}
}
// --- Функции для API остаются без изменений ---
export async function getMovieById(id) {
const row = await db.get('SELECT data FROM movies WHERE id = ?', id);
return row ? JSON.parse(row.data) : null;
}
export async function getRandomMovie() {
const row = await db.get('SELECT data FROM movies ORDER BY RANDOM() LIMIT 1');
return row ? JSON.parse(row.data) : null;
}
export async function getMovies(options) {
// Эта функция остается без изменений, так как она уже была достаточно оптимизирована для чтения
const { filters, pagination, sorting } = options;
let whereClauses = [];
let params = [];
let joins = new Set();
for (const key in filters) {
let value = filters[key];
if (!value) continue;
if (['year', 'rating.kp', 'rating.imdb', 'ageRating', 'movieLength'].includes(key)) {
const dbKey = key.replace('.', '_');
const [min, max] = value.toString().split('-').map(parseFloat);
if (!isNaN(min)) {
whereClauses.push(`m.${dbKey} >= ?`);
params.push(min);
}
if (max && !isNaN(max)) {
whereClauses.push(`m.${dbKey} <= ?`);
params.push(max);
}
}
else if (['type', 'isSeries'].includes(key)) {
const dbKey = key.replace('.', '_');
const values = Array.isArray(value) ? value : [value];
whereClauses.push(`m.${dbKey} IN (${values.map(() => '?').join(',')})`);
params.push(...values);
}
else if (key === 'genres.name' || key === 'countries.name') {
const isGenre = key === 'genres.name';
const linkTable = isGenre ? 'movie_genres' : 'movie_countries';
const mainTable = isGenre ? 'genres' : 'countries';
const linkAlias = isGenre ? 'mg' : 'c'; // Уникальные алиасы
const mainAlias = isGenre ? 'g' : 'co';
const values = Array.isArray(value) ? value : [value];
let includeClauses = [];
let includeParams = [];
for (const v of values) {
if (v.startsWith('+')) {
const name = v.substring(1);
whereClauses.push(`EXISTS (SELECT 1 FROM movie_genres mg_in JOIN genres g_in ON mg_in.genre_id = g_in.id WHERE mg_in.movie_id = m.id AND g_in.name = ?)`);
params.push(name);
} else if (v.startsWith('!')) {
const name = v.substring(1);
whereClauses.push(`NOT EXISTS (SELECT 1 FROM movie_genres mg_ex JOIN genres g_ex ON mg_ex.genre_id = g_ex.id WHERE mg_ex.movie_id = m.id AND g_ex.name = ?)`);
params.push(name);
} else {
includeClauses.push('?');
includeParams.push(v);
}
}
if(includeClauses.length > 0){
whereClauses.push(`EXISTS (SELECT 1 FROM movie_genres mg_inc JOIN genres g_inc ON mg_inc.genre_id = g_inc.id WHERE mg_inc.movie_id = m.id AND g_inc.name IN (${includeClauses.join(',')}))`);
params.push(...includeParams);
}
}
}
const whereString = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
const joinString = Array.from(joins).join(' ');
const totalResult = await db.get(`SELECT COUNT(DISTINCT m.id) as count FROM movies m ${joinString} ${whereString}`, params);
const total = totalResult.count;
let orderString = 'ORDER BY m.id';
if (sorting && sorting.field && sorting.type) {
const sortField = sorting.field.replace('.', '_');
const sortType = sorting.type === '-1' ? 'DESC' : 'ASC';
if (['id', 'year', 'rating_kp', 'rating_imdb'].includes(sortField)) {
orderString = `ORDER BY m.${sortField} ${sortType}`;
}
}
const limit = pagination.limit || 10;
const offset = ((pagination.page || 1) - 1) * limit;
const query = `SELECT DISTINCT m.data FROM movies m ${joinString} ${whereString} ${orderString} LIMIT ? OFFSET ?`;
const results = await db.all(query, [...params, limit, offset]);
return {
docs: results.map(r => JSON.parse(r.data)),
total
};
}