Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Как я с нуля собрал кино-сервис с рекомендациями: Node.js, SQLite и немноголинейной алгебры

Дата публикации: 23-10-2026 15:26:28

Знакомая ситуация: вечер, вы вдвоём или компанией хотите посмотреть кино — и следующие сорок минут листаете каталоги стримингов. Рекомендации «по вашим просмотрам» тут не помогают: смотрит-то компания, а не один человек.Разбираем архитектуру KINO PARTY (partykino.com) — каталога фильмов и сериалов на 900 000 тайтлов с рекомендациями без нейросетей и ML-инфраструктуры: разрежённые векторы, косинусное сходство, инвертированный индекс, real-time комнаты кино-свайпера на SSE и честное SEO. Читать далее

Основное содержимое страницы с новостью.

Уровень сложностиСредний

Время на прочтение13 мин

Охват и читатели1K

Кейс

Знакомая картина: суббота, вечер, вы собрались вдвоем или небольшой компанией посмотреть кино. И следующие сорок минут уходят на тоскливое листание каталогов. Один хочет комедию, второй терпеть не может глупый юмор, третий уже пересмотрел все новинки. Персональные рекомендации стримингов здесь бесполезны: они заточены под вкусы одного конкретного профиля, а не под разношерстную группу людей на одном диване.

Из этого раздражения и вырос KINO PARTY (partykino.com) — двуязычный каталог фильмов и сериалов, в котором на первом месте стоит не поиск по названию, а подбор: похожие картины с понятным объяснением сходства, фильтр по эмоциональному состоянию, подбор для компании и кино-свайпер — формат тиндера для фильмов, где друзья свайпают карточки в реальном времени, а сервис находит общие совпадения.

В этом материале разберем архитектуру сервиса изнутри:

  • Как проект вырос из первого наброска до каталога на 900 000 тайтлов;

  • Как работает рекомендательный движок без нейросетей, векторных баз и Python-сервисов — на одном SQLite и инвертированном индексе;

  • Откуда берутся проценты совпадения, объяснение «почему фильм подходит» и математика настроения;

  • С какими граблями пришлось столкнуться в данных: конфликт ID Кинопоиска и TMDB, сотни тысяч фальшивых «русских» описаний и лимиты внешних API;

  • Как выстроить SEO для огромной базы, где 97% страниц рискуют попасть под фильтр за тонкий контент;

  • Практика разработки в связке с ИИ-агентами: реальная польза и факапы вроде выдуманных рейтингов.

Весь бэкенд крутится на Node.js 24, фронтенд написан на чистом ванильном JavaScript без фреймворков и сборщиков.


1. С чего все начиналось

Первый коммит в репозитории появился после простой мысли: нужен сайт с базой фильмов, но с упором на совместный выбор, без тонн рекламы и навязчивых баннеров. Начиналось все как пет-проект под собственные нужды — когда каждые выходные тратишь час на выбор фильма, хочется иметь инструмент, решающий задачу за пару минут. За несколько месяцев проект развился в полноценный продакшн с мобильным приложением и базой почти в миллион тайтлов на собственном VPS.

Стек технологий подбирался с прицелом на автономность и минимальные накладные расходы:

Слой

Технологии

Сервер

Node.js 24 + Express

База данных

SQLite через нативный node:sqlite (DatabaseSync), поиск через FTS5

Фронтенд

Чистый JS, собственная легкая система локализации (RU/EN), без сборщиков

Real-time

Server-Sent Events (SSE), сессионные комнаты в оперативной памяти

Аутентификация

Firebase Auth

Мобильное приложение

Capacitor (iOS и Android на базе того же веб-интерфейса)

Источники данных

TMDB (метаданные, постеры, персоны), OMDb (рейтинги IMDb, расширенный сюжет), Wikidata (ID проектов в Netflix)

Выбор SQLite был вполне осознанным. В режиме WAL база без проблем держит сотни тысяч записей, а стандартный модуль node:sqlite избавил от головной боли с нативными бинарными зависимостями вроде better-sqlite3. Сейчас в рабочей базе около 895 000 тайтлов, файл вместе с индексами и FTS5 занимает примерно 9 ГБ.

Базовое правило, определившее архитектуру: продовая база постоянно обновляется и в разы больше локальной. Любая логика рекомендаций и поиска обязана работать на лету за миллисекунды, не требуя тяжелых ночных пересчетов, фоновых очередей на часы или миграций по всей базе.


2. Разборки с данными

Качество рекомендаций напрямую упирается в чистоту данных. Именно на очистку и сведение метаданных ушла львиная доля времени.

2.1. Формирование каталога

Основной поток метаданных идет из ежедневных экспортов TMDB (movie_ids_...json.gz, tv_series_ids_...json.gz). Скрипт импорта забирает идентификаторы и для каждого проекта делает запросы на двух языках (русский и английский) с параметром append_to_response=credits,external_ids,images. Двуязычность закладывалась сразу: русская версия идет на основной домен, английская доступна по префиксу /en/*.

Очередь импорта лежит в той же SQLite. Если процесс прерывается или падает, он без потерь продолжает работу с последней позиции. Важный нюанс: фильтрация по популярности прямо на этапе экспорта. Если тянуть все подряд, база моментально забьется мусорными любительскими короткометражками с нулем оценок.

OMDb закрывает пробелы TMDB: оттуда берутся рейтинги IMDb, число голосов, награды, прокатный рейтинг MPAA, Metascore и кассовые сборы. А прямые ссылки на просмотр в Netflix я вытягиваю из Wikidata по свойству P1874 (через IMDb ID): коммерческие API поиска по стримингам часто отваливаются или требуют неоправданных подписок.

2.2. Конфликт идентификаторов Кинопоиска и TMDB

Исторически первая версия базы собиралась по ID Кинопоиска, а последующие обновления пошли из TMDB. В итоге в поле id оказались смешаны идентификаторы из двух разных пространств.

Классический пример: у культовой «Матрицы» на Кинопоиске ID 301, а в TMDB — 603. При этом в базе уже существовал другой фильм с ID 603 («Салон Вероники»). Когда скрипт связывал фильмографию актера из TMDB простым JOIN movies ON id = tmdbId, Киану Ривз внезапно оказывался в польской драме.

Решение потребовало двух шагов:

  1. Выделили отдельную колонку tmdbId, заполнив ее через эндпоинт поиска /find/{imdbId}.

  2. Ввели строгую проверку соответствия года выпуска и типа проекта: если ID совпал, но год отличается более чем на один, связь отбрасывается. При выводе фильмографии приоритет отдается проверенному tmdbId, и только при его отсутствии идет откат к базовому id.

2.3. Фальшивые русские описания

Казалось бы, мелочь: на русскоязычной странице фильма отображается текст на английском. Но масштаб проблемы открылся только после прямого SQL-запроса.

Выяснилось, что у 669 416 фильмов из 762 000 с заполненным описанием в «русской» колонке не было ни единого кириллического символа. Текст полностью совпадал с английским оригиналом. Загрузчик TMDB при отсутствии русского текста молча дублировал английский в оба поля.

Переводить миллион записей подряд бессмысленно и дорого. Я ограничил задачу только ядром каталога — 26 880 страницами с подтвержденным качеством и трафиком. 4 404 описания перевели через API перевода. Тут же прилетел лимит Google Translate с ошибками 429, подтвердив, что бесплатные эндпоинты не годятся для продакшна.

Еще один забавный артефакт: переводчик обильно внедрял в текст невидимые пробелы нулевой ширины (U+200B). В нескольких сотнях описаний они сидели в базе, пока не проявились странными разрывами слов в верстке.

2.4. Обогащение сюжетов

Для работы рекомендаций и семантического поиска описания ядра каталога расширили ключевыми словами TMDB (keywords вроде «time loop», «dystopia», «cyberpunk») и полными синопсисами из OMDb (plot=full).

Скрипт обогащения написан с защитой от сбоев: отмечает обработанные ID, не трогает строки при ошибках сети и немедленно останавливается при исчерпании дневной квоты.

На этом этапе я наступил на грабли: первый прогон бодро отрапортовал об успехе, но расширенные сюжеты появились лишь у тысячи фильмов. Бесплатный ключ OMDb уперся в лимит 1 000 запросов в сутки, а скрипт принял служебную строку Request limit reached! за валидный ответ и пометил фильмы как готовые.

Урок простой: любой фоновый скрипт, работающий со сторонними API, обязан строго разделять ответы «данные отсутствуют» и «запрос отклонен из-за лимита».

Итог по ядру в 26 880 фильмов: поле tmdbId заполнено у 99,5%, ключевые слова есть у 83,8%, полный сюжет — у 99,3%. При этом расширенный синопсис на сайте выводится только тогда, когда он хотя бы на четверть длиннее краткого описания, чтобы не показывать пользователю дублирующий текст другими словами.


3. Рекомендательный движок: чистая линейная алгебра на SQLite

Все варианты рекомендаций — похожие фильмы, персональная лента, тест настроения и групповой кино-свайпер — обслуживает единый модуль (server/recommender.js, порядка 650 строк кода). Индекс строится в памяти при старте приложения.

3.1. Почему первая версия оказалась провалом

Сначала рекомендации строились на наборе ручных эвристик вида score += 10. Когда мы начали тестировать выдачу, фильмы казались неадекватными. Аудит логики вскрыл пять классических проблем «наивных» рекомендателей:

  1. Теги без энтропии. На 3 000 тестовых фильмов пришлось всего 1 122 уникальных комбинации тегов. Тег «моральный выбор» стоял буквально у каждого фильма, давая нулевую различительную способность.

  2. Мертвые ветки логики. Тест онбординга отдавал английские ключи (drive, fun), а в базе лежали русские строки. Условие tags.includes() просто никогда не срабатывало.

  3. Игнорирование реальных действий. Таблицы лайков и просмотров аккуратно наполнялись, но код рекомендаций их даже не запрашивал.

  4. Рисованные проценты. В коде стояла заглушка max(74, calculatedScore): пользователь при любых раскладах видел совпадение от 74% до 99%, даже на случайном мусоре.

  5. Ранжирование рейтингом вместо релевантности. Запрос делал ORDER BY rating DESC LIMIT 200, а математика лишь слегка двигала позиции на ±10 пунктов. Реальную выдачу определял тупой топ по оценкам.

Если вы пишете собственный рекомендатель, проверьте его по этим пунктам в первую очередь: учитываются ли действия пользователя, честны ли отображаемые цифры и не подменяет ли сортировка по рейтингу саму идею подбора.

3.2. Векторная модель фильма

В переписанном движке каждый фильм описывается нормализованным разреженным вектором. В него входят только устойчивые признаки с понятным вкладом:

Префикс

Признак

Базовый вес

Откуда берется

g:

Жанр

3.6

Поле genres — самый надежный сигнал

k:

Ключевые слова

tf·idf × 0.42

Топ-20 терминов из описания (RU+EN)

m:

Эмоциональный тон

0.7–1.2

Тон повествования с поправкой на интенсивность

n:

Двигатель сюжета

≈ 0.9

Тип конфликта

e: / p:

Эпоха / локация

0.45 / 0.35

Сеттинг картины

d:

Десятилетие

0.55

Год выпуска

c:

Страна производства

0.5

Страна

t:

Формат

0.15–0.6

Полный метр или сериал

Обратите внимание на вес жанра — 3.6 против 0.4–1.2 у остальных признаков. Анализ энтропии показал, что именно жанровая принадлежность дает самый чистый сигнал, тогда как смысловые и настроенческие теги служат лишь тонкой доводкой. Ключевые слова отбираются по классическому TF-IDF: слова, встречающиеся более чем в 30% каталога, и редкие термины с частотой меньше 5 отсекаются как шум.

Построение вектора компактно укладывается в одну функцию:

function vectorFromFeatures(f, idf) {
  const vec = Object.assign({}, f.vec);
  const scored = [];
  for (const w in f.kw) {
    const iw = idf.get(w);
    if (!iw) continue;
    scored.push([w, Math.log(1 + f.kw[w]) * iw]);
  }
  scored.sort((a, b) => b[1] - a[1]);
  scored.slice(0, 20).forEach(([w, wt]) => addFeat(vec, 'k:' + w, wt * 0.42));
  normalize(vec);
  return vec;
}
3.3. Расчет сходства через инвертированный индекс

Степень схожести фильмов оценивается по косинусному расстоянию. Поскольку все векторы предварительно нормализованы к единичной длине, косинус сводится к скалярному произведению.

Считать попарное произведение со всей базой на каждый чих недопустимо. Движок строит в памяти инвертированный индекс вида признак -> [[id, вес], ...]. При запросе перебираются только те тайтлы, которые делят с исходным фильмом хотя бы один общий признак:

function rankByVector(index, query, { excludeIds } = {}) {
  const scores = new Map();
  for (const feat in query) {
    const postings = index.invIndex.get(feat);
    if (!postings) continue;
    const qw = query[feat];
    for (let i = 0; i < postings.length; i++) {
      const id = postings[i][0];
      if (excludeIds && excludeIds.has(id)) continue;
      scores.set(id, (scores.get(id) || 0) + qw * postings[i][1]);
    }
  }
  return scores;
}

Для выборки в 15 000 ключевых фильмов индекс содержит около 7 000 признаков. Построение занимает полсекунды при старте процесса, а поиск похожих выполняется за 1–3 миллисекунды. Результаты кэшируются по ID.

Поверх базового косинуса накладываются небольшие эвристические корректировки (общая страна +0.03, близкий год выпуска в пределах 8 лет +0.03, близкий рейтинг +0.02) и обязательный штраф −0.25 за сиквелы. Если первые два слова названий совпадают, перед нами следующая часть франшизы: пользователю не нужно забивать блок похожих четырьмя частями одного и того же фильма.

3.4. Честные проценты и объяснение выбора

Мы полностью отказались от искусственного нижнего порога процентов. Отображаемое значение рассчитывается из фактического косинуса:

  • Внутренний уровень совпадения делится на категории: perfect (≥ 0.50), strong (≥ 0.35), good (≥ 0.22) и fair.

  • Визуальный процент считается через нелинейную формулу: round(22 + 74 * Math.sqrt(sim)). Корень растягивает различия в верхней части диапазона, сохраняя строгое убывание в списке.

Главная деталь интерфейса — токены объяснения (whyTokens). Движок сохраняет три признака с наибольшим вкладом в скалярное произведение и превращает их в строку: «Похоже по: драма · детектив · теплая атмосфера». Это ничего не стоит с точки зрения вычислений, но кардинально меняет восприятие выдачи: человек видит логику алгоритма, а не взятую с потолка цифру.

3.5. Персональные подборки

Вектор интересов конкретного пользователя формируется из двух компонентов:

  1. История лайков. Взвешенное среднее векторов оцененных фильмов, где свежие отметки имеют максимальный вес, а старые плавно угасают по экспоненте.

  2. Онбординг. Короткий опросник по предпочтениям (желаемый ритм, ключевые жанры, глубина сюжета). Ответ «хочу подумать» добавляет веса детективу (×1.4), триллеру (×0.7) и сюжетам о расследованиях (×0.5).

Если у пользователя уже есть лайки, вектор микшируется в пропорции 0.62 * лайки + 0.38 * опрос. При отсутствии лайков работает только опрос, а для неавторизованных гостей включается cold-start: сбалансированный топ по рейтингу с жестким контролем жанрового разнообразия, чтобы первый экран не превращался в парад однотипных боевиков.

3.6. Подбор по настроению: модель Valence-Arousal

Для подбора под настроение мы использовали модель двух осей из психологии: валентность (уровень удовольствия от негативного к позитивному) и активация (уровень возбуждения нервной системы от спокойствия к драйву).

В начале теста пользователь отвечает на четыре базовых вопроса с оценкой от 0 до 4:

valence = (q1 + q2) - (q3 + q4); // диапазон от -8 до 8: эмоциональный фон
arousal = (q1 + q3) - (q2 + q4); // диапазон от -8 до 8: уровень энергии

Эти координаты напрямую модифицируют целевой вектор поиска:

  • Низкая валентность (≤ −2, подавленное состояние) повышает приоритет светлых и теплых историй, накладывая штрафы на мрачные драмы и хорроры;

  • Высокая активация (≥ 3, прилив энергии) смещает выдачу в сторону динамичных триллеров и экшена, а низкая (≤ −3) — в сторону созерцательных драм и документалистики;

  • Жесткий стоп-лист («только без насилия») полностью вырезает соответствующие категории из пула кандидатов.

3.7. Групповой режим: объединение вкусов компании

Тест настроения можно проходить в складчину. В коде заложены три правила объединения анкет:

  1. Векторы всех участников суммируются, формируя усредненный центр интересов;

  2. Стоп-листы объединяются по принципу жесткого «ИЛИ»: если хотя бы один человек исключил триллеры, триллеров не будет ни у кого;

  3. Участник с наиболее подавленным настроением (минимальная валентность) получает повышающий коэффициент веса (×1.6). Если кому-то в компании откровенно тоскливо, тяжелый или циничный фильм окончательно испортит вечер. Компания легче перенесет легкое нейтральное кино, чем расстроенный человек — триггерный контент.


4. Проблемы масштабирования индекса

Пока база была компактной, все работало моментально. С ростом каталога начались технические сложности.

4.1. GC-штормы при раздувании памяти

Изначально мы попытались загнать в индекс 50 000 самых популярных тайтлов. На проде это привело к периодическим зависаниям процесса Node.js и ошибкам 504 Gateway Timeout: движок V8 тратил секунды на сборку мусора в огромном графе связанных объектов Map и массивов постингов.

Размер индекса урезали до 15 000 ключевых картин. Это полностью сняло нагрузку по памяти и времени ответа, но породило новую проблему.

4.2. Холодный старт для новинок

Свежие премьеры еще не успели набрать тысячи голосов и не попадают в топ-15 000 индекса. В итоге у нового фильма не оказывалось собственного вектора, и блок похожих картин оставался пустым.

Мы решили это генерацией вектора на лету: если запрашиваемого ID нет в предрассчитанном индексе, функция собирает его признаки прямо из строки базы данных и прогоняет через инвертированный индекс кандидатов:

function similarForRow(index, row, { limit = 8 } = {}) {
  if (index.movieVec.has(row.id)) return similar(index, row.id, { limit });
  const q = vectorFromFeatures(rawFeatures(row), index.idf);
  return similarFromVector(index, row.id, q, metaFromRow(row), limit);
}

Индекс не раздувается, память стабильна, а блок похожих гарантированно работает для любого из 895 000 тайтлов.

4.3. Сериализация индекса на диск

Генерация даже 15 000 векторов с нуля занимает несколько секунд, в течение которых сервер не может отвечать на входящие запросы. Готовый индекс кэшируется на диске в файле .reco-cache.json (около 38 МБ). В файл записывается хэш валидности: число записей в базе и максимальный ID. При рестарте инстанса кэш читается за десятые доли секунды.


5. Кино-свайпер: real-time Tinder для фильмов

Одной из самых востребованных функций стал кино-свайпер (partykino.com/swiper). Создается комната, друзья подключаются по короткому коду или QR-коду и листают одну и ту же колоду карточек. Сервис отслеживает совпадения в реальном времени.

5.1. Архитектура без постоянной базы и WebSockets

Мы намеренно отказались от сохранения игровых сессий в постоянную БД и от тяжелого протокола WebSocket:

  • Сессия живет максимум пару часов. Все состояние комнаты хранится в оперативной памяти сервера в обычной структуре Map со временем жизни 2 часа;

  • Синхронизация построена на Server-Sent Events (SSE). Клиентам нужен только односторонний канал уведомлений от сервера (старт сессии, фиксация совпадения, завершение колоды), а сами свайпы уходят стандартными HTTP POST-запросами. SSE нативен для браузера, автоматически восстанавливает соединение и не требует сторонних библиотек;

  • Важная деталь: поток SSE нельзя пропускать через стандартное gzip-сжатие Express. Ответы с заголовком сжатия буферизуются прокси-сервером Nginx, и события приходят пачками с задержкой. Для эндпоинта стрима обязательно отдается заголовок Cache-Control: no-transform.

5.2. Правило фиксации совпадения

Порог согласия зависит от размера группы:

function matchThreshold(memberCount) {
  if (memberCount <= 2) return 2;         // пара: нужно единогласие
  return Math.floor(memberCount / 2) + 1; // 3+ участников: простое большинство
}

Для пары требуется согласие обоих партнеров. Для компании от трех человек достаточно строгого большинства. В конце раунда участники получают общий список совпадений, отсортированный по числу голосов.


6. SEO для базы на 900 000 страниц

Огромный каталог — прямая дорога под санкции поисковых систем за низкокачественный контент. У подавляющего большинства фильмов в открытых базах нет ничего, кроме названия, года и пары сухих строк.

Мы внедрили жесткий фильтр качества (server/seo.js): страница отдается поисковикам на индексацию только при выполнении критериев:

verified = 1
AND description IS NOT NULL AND length(description) >= 100
AND poster IS NOT NULL AND poster != ''
AND votes >= 100

Из 895 000 тайтлов фильтр пропускает около 27 000 страниц (примерно 3%). Только они попадают в файлы sitemap и отдаются с разрешающими метатегами. Все остальные страницы физически доступны пользователям по прямым ссылкам, но получают метатег noindex, follow.

Дополнительно реализовано:

  • Полноценный SSR первого экрана. Поисковый робот получает готовый HTML-документ с заголовком <h1>, описанием, актерским составом и постером без необходимости исполнения клиентского JS;

  • Разбивка Sitemap. Индексный файл ссылается на чанки строго по 5 000 URL, что исключает таймауты при генерации и парсинге;

  • Честный lastmod. Дата модификации выставляется только тогда, когда в базе есть реальная дата изменения записи. Подсовывать текущее системное время роботу бессмысленно: поисковики быстро распознают обман и перестают доверять карте сайта.

Поучительная история произошла на страницах-обзорах стриминговых сервисов. Один из ИИ-агентов, писавших код интерфейса, самовольно внедрил туда микроразметку aggregateRating с оценками и тысячами отзывов, которых никто не оставлял, снабдив страницу фразой «сервис протестирован нашей редакцией». За такое творчество Google моментально вешает ручные санкции на весь домен. Фейковую разметку мы оперативно удалили, оставив только корректный BreadcrumbList и честное указание на субъективный рейтинг редакции.


7. Разработка в тандеме с ИИ-агентами

Код проекта писался при активном участии ИИ-агентов (Antigravity и Claude Code), которые вносили правки параллельно.

Что доказало свою эффективность:

  • Пошаговая проверка. Запуск скрипта на одном-двух конкретных ID (--id=301,1250), ручная проверка результата глазами и только затем массовая обработка.

  • Взаимный аудит диффов. Когда один агент дорабатывает сложную логику, полезно скормить получившийся diff другому агенту с задачей «найди критические ошибки и узкие места». Так мы выловили зависание кэша sitemap и состояние гонки при обновлении словаря фильмографии.

Главные зоны риска:

  • Утечки секретов: агент без задней мысли может захардкодить API-ключ в тело скрипта или пробросить его в открытый JSON-ответ контроллера. Ключи должны жестко контролироваться через переменные окружения .env.

  • Логические галлюцинации: создание красивых структур данных, которые нигде не читаются, или добавление ложных метрик в разметку.


Что в итоге

На текущий момент KINO PARTY работает как стабильный сервис:

  • Каталог на 895 000 тайтлов с фильтрацией качественного ядра;

  • Легковесный движок рекомендаций на SQLite, отвечающий за миллисекунды;

  • Персональные профили вкусов, кино-тиндер для компании и подбор по настроению;

  • Мобильные сборки под iOS и Android на базе Capacitor;

  • Корректная поисковая оптимизация без мусорного спама.

Следующий крупный шаг — внедрение семантического поиска по сюжетным деталям («фильм, где герой застрял в поезде и ищет бомбу»), где компактные эмбеддинги будут дополнять работу классического инвертированного индекса.

Сайт доступен по адресу partykino.com. Буду рад отзывам, конструктивной критике архитектуры и предложениям по развитию сервиса в комментариях.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Чат на сайте стоит дороже, чем кажется: как я замерил его цену и переписал виджет под результат09.5828-07-2026
2Платформа данных на минималках. Часть 2. Практика: разворачиваем и настраиваем каталог метаданных08.3519-08-2026
3За полчаса и 10 рублей — полноценный сайт для загрузки и редактирования скриншотов на вайбкоде. Пошаговая инструкция07.6629-07-2026
4Рерайт 5 фильмов/сериалов №334 /1 - 800 знаков(без учета пробелов). - Без использования ChatGPT и аналогов112.8619-08-2026
5Scrutor: логирование отменённых HTTP-запросов09.5720-10-2026
6«Это уже тысячу раз делали»: как мы добавили медиаленту в Яндекс Еду для iOS. А потом переделали08.4619-06-2026
7Рерайт 5 фильмов/сериалов №673/6 - 800 знаков(без учета пробелов). - Без использования ChatGPT и аналогов112.0919-08-2026
8Как получить пожизненный бан на VC.ru за open-source проект на Node.js без рекламы. Кейс CMTT09.6429-07-2026
9Рерайт 5 фильмов/сериалов №674/6 - 800 знаков(без учета пробелов). - Без использования ChatGPT и аналогов112.0919-08-2026
10Кто в кино? 🍿 Собрали премьеры «КИНО СИТИ», ради которых ...07.424-08-2026

Классификация: . Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 14.66. Источник: habr.com.