itube-specs 0.0.1059 → 0.0.1060

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SITE_UPDATES.md CHANGED
@@ -11,6 +11,18 @@
11
11
  коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
12
12
  кому именно (всем / сайтам с N), что будет, если не сделать.
13
13
 
14
+ ## 0.0.1060 — 2026-11-02
15
+
16
+ - **Сайтам со своими `assets/scss/vars` (hdsex-remaster и др.), срочно:** с 0.0.1059 (лоадер, `0d412182`) без новых
17
+ токенов сборка падает на `Undefined variable`. В `_vars.scss`: `$blur-loading: 4px;` и
18
+ `$blur-loading-spill: $blur-loading * 3;`. В теме: `$loading-bg` (тёмная — `transparent`, светлая —
19
+ `$color-white-75`) и `$video-preview-loading-bg: transparent;`. Образец — hdsex-remaster.
20
+
21
+ - **Всем — к сведению:** в строке чипсов над плеером — только топ-категории ролика (`categories[].isTop`, ставит BFF
22
+ видео по закешированному `/v1/categories/top`, к бекенду — раз в 10 минут на домен/язык/нишу). Нетоповые остаются в
23
+ блоке под описанием и не возвращаются в строку тегом. Топ-список не пришёл — как раньше, все категории. Делать ничего
24
+ не надо.
25
+
14
26
  ## 0.0.1059 — 2026-10-30
15
27
 
16
28
  - **Всем, срочно к сведению:** с 0.0.1056 блок Live Cams и врезки вебкамов пропали из сеток видео на страницах, которые
@@ -234,6 +234,11 @@ const categoriesChips = computed((): IChipsItem[] => props.data.categories && pr
234
234
  prefix: 'categories',
235
235
  })));
236
236
 
237
+ // Над плеером — только топ-категории (`isTop` ставит BFF); без пометки (топ-список не пришёл) — все.
238
+ // Теги, совпавшие с нетоповой категорией, всё равно отсекаются по полному списку ниже.
239
+ const topCategoriesChips = computed((): IChipsItem[] => (categoriesChips.value ?? [])
240
+ .filter((_, index) => props.data.categories[index]?.isTop !== false));
241
+
237
242
  const categoriesTitles = computed<string[]>(() => props.data.categories && props.data.categories?.map(item => item.title?.replace(/\s+/g, '').toLowerCase() ?? ''));
238
243
 
239
244
  const withoutCategoryTitles = (chips: IChipsItem[]) => (categoriesTitles.value?.length
@@ -258,9 +263,9 @@ const topChips = computed(() => {
258
263
  prefix: 'search',
259
264
  } as IChipsItem));
260
265
  } else if (tagGroupChips.value.length) {
261
- return [...categoriesChips.value || [], ...withoutCategoryTitles(tagGroupChips.value)];
266
+ return [...topCategoriesChips.value, ...withoutCategoryTitles(tagGroupChips.value)];
262
267
  } else if (categoriesChips.value || filterTagChips.value) {
263
- return [...categoriesChips.value || [], ...filterTagChips.value || []];
268
+ return [...topCategoriesChips.value, ...filterTagChips.value || []];
264
269
  }
265
270
  return null;
266
271
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.1059",
4
+ "version": "0.0.1060",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
package/runtime/index.ts CHANGED
@@ -46,6 +46,7 @@ export * from './utils/cleaners/clean-video-card';
46
46
  export * from './utils/cleaners/clean-model-info';
47
47
  export * from './utils/cleaners/clean-suggest-models';
48
48
  export * from './utils/video-data-add-model-portraits';
49
+ export * from './utils/video-data-mark-top-categories';
49
50
  export * from './utils/converters/convert-categories-to-footer';
50
51
  export * from './utils/converters/convert-categories-to-chips';
51
52
  export * from './utils/converters/convert-tag-group-item-to-chips';
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { IRawVideoData } from '../../../types';
3
+ import { videoDataMarkTopCategories } from '../video-data-mark-top-categories';
4
+
5
+ const video = {
6
+ categories: [{ name: 'cartoon', title: 'Cartoon' }, { name: '3d', title: '3D' }, { name: 'Masturbation', title: 'Masturbation' }],
7
+ } as IRawVideoData;
8
+
9
+ describe('videoDataMarkTopCategories', () => {
10
+ it('marks categories from the top list, ignoring case and edge spaces', () => {
11
+ const result = videoDataMarkTopCategories(video, ['cartoon', 'masturbation ']);
12
+
13
+ expect(result.categories.map(category => category.isTop)).toEqual([true, false, true]);
14
+ });
15
+
16
+ it('leaves the video untouched without the top list', () => {
17
+ expect(videoDataMarkTopCategories(video, null)).toBe(video);
18
+ });
19
+ });
@@ -0,0 +1,21 @@
1
+ import type { IRawVideoData } from '../../types';
2
+
3
+ const normalize = (name: string) => name.trim().toLowerCase();
4
+
5
+ /**
6
+ * Помечает категории ролика `isTop` по списку топ-категорий: в строку над плеером идут только топовые.
7
+ * Списка нет (ручка упала) — ролик без пометок, строка покажет все категории, как раньше.
8
+ * Имена сравниваются без регистра и краевых пробелов: в API встречается `masturbation `.
9
+ * @param videoData - сырые данные видео
10
+ * @param topNames - имена топ-категорий
11
+ */
12
+ export function videoDataMarkTopCategories(videoData: IRawVideoData, topNames: string[] | null): IRawVideoData {
13
+ if (!videoData?.categories?.length || !topNames) return videoData;
14
+
15
+ const top = new Set(topNames.map(normalize));
16
+
17
+ return {
18
+ ...videoData,
19
+ categories: videoData.categories.map(category => ({ ...category, isTop: top.has(normalize(category.name)) })),
20
+ };
21
+ }
@@ -1,5 +1,5 @@
1
1
  import type { IRawVideoData, IVideoData, IRawModelCard } from '../../../../types';
2
- import { cleanVideoData, videoDataAddModelPortraits, getUrlWithProxiedParams } from '../../../../runtime';
2
+ import { cleanVideoData, videoDataAddModelPortraits, videoDataMarkTopCategories, getUrlWithProxiedParams } from '../../../../runtime';
3
3
 
4
4
  export default defineEventHandler(async (event): Promise<IVideoData> => {
5
5
  const theme = useRuntimeConfig(event).public.theme;
@@ -17,15 +17,16 @@ export default defineEventHandler(async (event): Promise<IVideoData> => {
17
17
  const videoUrl = getUrlWithProxiedParams(`${apiUrl}/v2/video/url/${id}`, event);
18
18
  const modelsUrl = getUrlWithProxiedParams(`${apiUrl}/v2/models/list-by-video-id/${id}`, event);
19
19
 
20
- const [videoRaw, modelsRaw] = await Promise.all([
20
+ const [videoRaw, modelsRaw, topCategoryNames] = await Promise.all([
21
21
  apiFetch(videoUrl, {
22
22
  method: 'GET',
23
23
  headers: clientHeaders,
24
24
  }),
25
25
  apiFetch(modelsUrl, { method: 'GET', headers: clientHeaders }).catch(() => null),
26
+ getTopCategoryNames(event),
26
27
  ]);
27
28
 
28
- const videoData = withDemoFallbacks(videoRaw.data as IRawVideoData);
29
+ const videoData = videoDataMarkTopCategories(withDemoFallbacks(videoRaw.data as IRawVideoData), topCategoryNames);
29
30
  const modelsData = modelsRaw?.data as Record<'items', Array<IRawModelCard>>;
30
31
 
31
32
  return cleanVideoData(
@@ -1,4 +1,4 @@
1
- import { cleanVideoData, videoDataAddModelPortraits, getUrlWithProxiedParams } from '../../../../runtime';
1
+ import { cleanVideoData, videoDataAddModelPortraits, videoDataMarkTopCategories, getUrlWithProxiedParams } from '../../../../runtime';
2
2
  import type { IRawVideoData, IVideoData, IRawModelCard } from '../../../../types';
3
3
 
4
4
  export default defineEventHandler(async (event): Promise<IVideoData> => {
@@ -15,15 +15,16 @@ export default defineEventHandler(async (event): Promise<IVideoData> => {
15
15
  const videoUrl = getUrlWithProxiedParams(`${apiUrl}/v2/video/url/${url}`, event);
16
16
  const modelsUrl = getUrlWithProxiedParams(`${apiUrl}/v2/models/list-by-video-url/${url}`, event);
17
17
 
18
- const [videoRaw, modelsRaw] = await Promise.all([
18
+ const [videoRaw, modelsRaw, topCategoryNames] = await Promise.all([
19
19
  apiFetch(videoUrl, {
20
20
  method: 'GET',
21
21
  headers: clientHeaders,
22
22
  }),
23
23
  apiFetch(modelsUrl, { method: 'GET', headers: clientHeaders }).catch(() => null),
24
+ getTopCategoryNames(event),
24
25
  ]);
25
26
 
26
- const videoData = withDemoFallbacks(videoRaw.data as IRawVideoData);
27
+ const videoData = videoDataMarkTopCategories(withDemoFallbacks(videoRaw.data as IRawVideoData), topCategoryNames);
27
28
  const modelsData = modelsRaw?.data as Record<'items', Array<IRawModelCard>>;
28
29
 
29
30
  return cleanVideoData(
@@ -0,0 +1,14 @@
1
+ import type { H3Event } from 'h3';
2
+ import type { IRawCategoryCard } from '../../types';
3
+
4
+ /**
5
+ * Имена топ-категорий из кеша BFF (`cachedApiFetch`, 10 минут на домен + язык + нишу): к бекенду ходим
6
+ * раз в TTL, а не на каждый ролик. Ошибка ручки — `null`, ролик от этого не падает.
7
+ */
8
+ export async function getTopCategoryNames(event: H3Event): Promise<string[] | null> {
9
+ const url = `${process.env.NUXT_PUBLIC_API_DOMAIN}/v1/categories/top`;
10
+
11
+ return cachedApiFetch<Array<IRawCategoryCard>>(url, event)
12
+ .then(categories => (Array.isArray(categories) ? categories.map(category => category.name) : null))
13
+ .catch(() => null);
14
+ }
@@ -36,7 +36,9 @@ export interface IRawVideoData {
36
36
  sizeCache?: string,
37
37
  state?: string,
38
38
  title: string,
39
- unknownFields?: string
39
+ unknownFields?: string,
40
+ /** Не из API: ставит BFF по топ-списку категорий. */
41
+ isTop?: boolean
40
42
  }[];
41
43
  actions: null;
42
44
  primary_thumb_rank: number; // unused
@@ -82,6 +82,8 @@ export interface IVideoData {
82
82
  name: string;
83
83
  title: string;
84
84
  icon?: string;
85
+ /** Топ-категория — попадает в строку над плеером (BFF, по кешу `/v1/categories/top`). */
86
+ isTop?: boolean;
85
87
  }[];
86
88
  actions: null;
87
89
  channelName: string;