itube-specs 0.0.1054 → 0.0.1055
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 +15 -0
- package/app.config.ts +2 -0
- package/assets/scss/base/_loading.scss +13 -1
- package/assets/scss/themes/dark/_theme.scss +3 -0
- package/assets/scss/themes/light/_theme.scss +3 -0
- package/components/player/interface.vue +2 -1
- package/components/view/video.vue +8 -9
- package/composables/page/use-category-page.ts +9 -3
- package/i18n/locales/de-DE.json +1 -0
- package/i18n/locales/en-US.json +1 -0
- package/i18n/locales/es-ES.json +1 -0
- package/i18n/locales/fr-FR.json +1 -0
- package/i18n/locales/it-IT.json +1 -0
- package/i18n/locales/ja-JP.json +1 -0
- package/i18n/locales/ko-KR.json +1 -0
- package/i18n/locales/ru-RU.json +1 -0
- package/i18n/locales/zh-CN.json +1 -0
- package/package.json +1 -1
- package/runtime/utils/cleaners/__tests__/clean-video-data.test.ts +12 -18
- package/runtime/utils/cleaners/clean-video-data.ts +15 -21
- package/server/api/videos/get-video-by-id/[id].get.ts +3 -3
- package/server/api/videos/get-video-by-url/[url].get.ts +2 -3
- package/server/utils/video-demo-mock.ts +26 -0
- package/types/raw/raw-video-data.d.ts +3 -2
- package/types/video-data.d.ts +0 -1
- package/server/utils/video-tag-groups-mock.ts +0 -33
package/SITE_UPDATES.md
CHANGED
|
@@ -11,6 +11,21 @@
|
|
|
11
11
|
коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
|
|
12
12
|
кому именно (всем / сайтам с N), что будет, если не сделать.
|
|
13
13
|
|
|
14
|
+
## 0.0.1055 — 2026-10-30
|
|
15
|
+
|
|
16
|
+
- **Всем — к сведению:** related searches на странице категории (`useCategoryPage().relatedSearch`) теперь
|
|
17
|
+
отсортированы по убыванию `score` (раньше — в порядке ответа API). Новый ключ `appConfig.categoryRelatedSearchLimit`
|
|
18
|
+
(по умолчанию `null` — все пришедшие фразы, как было): число — показывать только топ-N по весу. Делать ничего не
|
|
19
|
+
надо; videosection-redesign ставит `20`, как на боевом videosection.
|
|
20
|
+
|
|
21
|
+
- **Всем — к сведению:** группы под плеером читаются из поля `entities` ответа видео v2 (бекенд: разделы категорий,
|
|
22
|
+
затем теги и поисковые фразы); `tags` снова только `string[]`. У узлов `tag`/`search` бекенд отдаёт в `title` сам
|
|
23
|
+
тип — заголовок берётся из переводов (новый ключ `tags`, и `related_searches`). BFF обеих ручек видео (по id и по
|
|
24
|
+
url) ходит в `GET /v2/video/url/{url}` — бекенд сам разбирает id или слаг; заглушка групп удалена. Временное демо (`server/utils/video-demo-mock.ts`): без описания — lorem, без разделов
|
|
25
|
+
категорий в `entities` — категории ролика одной группой «Categories»; уберём, когда данные появятся.
|
|
26
|
+
**Нужен бекенд с этой ручкой** (itube-gw `9d57077`): на API без неё страница видео отдаёт 500. Сайтам со своими локалями (hdsex `hi-IN`) — добавить `tags` и
|
|
27
|
+
`related_searches`, иначе заголовок будет на английском.
|
|
28
|
+
|
|
14
29
|
## 0.0.1054 — 2026-10-28
|
|
15
30
|
|
|
16
31
|
- **Сайтам со своей темой (hdsex-remaster):** в `_theme.scss` добавить токены тегов в раскрытом описании видео —
|
package/app.config.ts
CHANGED
|
@@ -14,6 +14,8 @@ export default defineAppConfig({
|
|
|
14
14
|
/** Есть ли у сайта сгруппированный вид категорий (`/categories` — группы, `/categories/all` — A–Z).
|
|
15
15
|
* `false` — `/categories` сам список A–Z: `CategoryNav` без табов Grouped / A-Z, «All» ведёт на `/categories`. */
|
|
16
16
|
categoriesGrouped: true,
|
|
17
|
+
/** Сколько связанных поисковых фраз (по убыванию веса) показывать на странице категории, `null` — все, что пришли. */
|
|
18
|
+
categoryRelatedSearchLimit: null as number | null,
|
|
17
19
|
/** Блок Live Cams в сетках листингов (при `FeatureWebcamsEnabled`), `null` — нет. `videos` — после какой строки
|
|
18
20
|
* карточек и сколько врезок вебкам-тумб (хвост того же списка) в сетке видео листинга (не похожие, не плейлист,
|
|
19
21
|
* не когда страница передала врезки сама); `models` — в основной сетке моделей (`priority`, не футер). */
|
|
@@ -21,6 +21,18 @@
|
|
|
21
21
|
display: block;
|
|
22
22
|
width: var(--size, 64px);
|
|
23
23
|
height: var(--size, 64px);
|
|
24
|
-
|
|
24
|
+
background-color: $loading-icon;
|
|
25
|
+
content: '';
|
|
26
|
+
mask-position: center;
|
|
27
|
+
mask-size: contain;
|
|
28
|
+
mask-repeat: no-repeat;
|
|
29
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><radialGradient id="a12" cx=".66" fx=".66" cy=".3125" fy=".3125" gradientTransform="scale(1.5)"><stop offset="0" stop-color="%23FFFFFF"></stop><stop offset=".3" stop-color="%23FFFFFF" stop-opacity=".9"></stop><stop offset=".6" stop-color="%23FFFFFF" stop-opacity=".6"></stop><stop offset=".8" stop-color="%23FFFFFF" stop-opacity=".3"></stop><stop offset="1" stop-color="%23FFFFFF" stop-opacity="0"></stop></radialGradient><circle transform-origin="center" fill="none" stroke="url(%23a12)" stroke-width="15" stroke-linecap="round" stroke-dasharray="200 1000" stroke-dashoffset="0" cx="100" cy="100" r="70"></circle><circle transform-origin="center" fill="none" opacity=".2" stroke="%23FFFFFF" stroke-width="15" stroke-linecap="round" cx="100" cy="100" r="70"></circle></svg>');
|
|
30
|
+
animation: loading-spin 2s linear infinite;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@keyframes loading-spin {
|
|
35
|
+
to {
|
|
36
|
+
transform: translate(-50%, -50%) rotate(-360deg);
|
|
25
37
|
}
|
|
26
38
|
}
|
|
@@ -87,7 +87,8 @@
|
|
|
87
87
|
:key="group.name || group.title"
|
|
88
88
|
class="player-interface__tags-group"
|
|
89
89
|
>
|
|
90
|
-
|
|
90
|
+
<!-- у узлов тегов и поисков бекенд кладёт в title сам тип — заголовок берём из переводов -->
|
|
91
|
+
<dt class="player-interface__tags-title">{{ group.type === 'category' ? group.title : t(group.type === 'tag' ? 'tags' : 'related_searches') }}</dt>
|
|
91
92
|
<dd class="player-interface__tags-list">
|
|
92
93
|
<UiChips
|
|
93
94
|
v-for="item in group.items"
|
|
@@ -183,8 +183,9 @@ const emit = defineEmits<{
|
|
|
183
183
|
|
|
184
184
|
const tagChips = computed((): IChipsItem[] => props.data.tags?.map(item => ({ title: item, value: item, prefix: 'tag' })) ?? []);
|
|
185
185
|
|
|
186
|
-
// v2:
|
|
186
|
+
// v2: теги и поиски из групп (без повторов). Категории — из `categories`: в группы попадают только те, у кого есть раздел
|
|
187
187
|
const tagGroupChips = computed((): IChipsItem[] => [...new Map((props.data.tagGroups ?? [])
|
|
188
|
+
.filter(group => group.type !== 'category')
|
|
188
189
|
.flatMap(group => group.items.map(item => convertTagGroupItemToChips(group.type, item)))
|
|
189
190
|
.map(chip => [`${chip.prefix}/${chip.value}`, chip])).values()]);
|
|
190
191
|
|
|
@@ -198,13 +199,11 @@ const categoriesChips = computed((): IChipsItem[] => props.data.categories && pr
|
|
|
198
199
|
|
|
199
200
|
const categoriesTitles = computed<string[]>(() => props.data.categories && props.data.categories?.map(item => item.title?.replace(/\s+/g, '').toLowerCase() ?? ''));
|
|
200
201
|
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
});
|
|
202
|
+
const withoutCategoryTitles = (chips: IChipsItem[]) => (categoriesTitles.value?.length
|
|
203
|
+
? chips.filter(chip => !categoriesTitles.value.includes(chip.title.replace(/\s+/g, '').toLowerCase()))
|
|
204
|
+
: chips);
|
|
205
|
+
|
|
206
|
+
const filterTagChips = computed(() => withoutCategoryTitles(tagChips.value));
|
|
208
207
|
|
|
209
208
|
const recentSearches = computed(() => {
|
|
210
209
|
if (props.recent && typeof props.recent === 'object' && Object.keys(props.recent).length === 0) {
|
|
@@ -225,7 +224,7 @@ const topChips = computed(() => {
|
|
|
225
224
|
prefix: 'search',
|
|
226
225
|
} as IChipsItem));
|
|
227
226
|
} else if (tagGroupChips.value.length) {
|
|
228
|
-
return tagGroupChips.value;
|
|
227
|
+
return [...categoriesChips.value || [], ...withoutCategoryTitles(tagGroupChips.value)];
|
|
229
228
|
} else if (categoriesChips.value || filterTagChips.value) {
|
|
230
229
|
return [...categoriesChips.value || [], ...filterTagChips.value || []];
|
|
231
230
|
}
|
|
@@ -27,7 +27,7 @@ export async function useCategoryPageData() {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Логика страницы категории поверх `useCategoryPageData()`: мета и 404, хлебные крошки, номер страницы,
|
|
30
|
-
* карточка инфо и чипсы связанных
|
|
30
|
+
* карточка инфо и чипсы связанных фраз (по убыванию `score`, не больше `appConfig.categoryRelatedSearchLimit`). Синхронная — вызывать в setup после `await useCategoryPageData()`.
|
|
31
31
|
*
|
|
32
32
|
* Мета по умолчанию — из API (`metaTitle` / `metaDescription`, без них — название), h1 — `header` или название.
|
|
33
33
|
* `metaKeys: true` — мета и h1 через `useMeta` по ключам `meta.<page>.<sort>.*` (`{text}` — название, с анти-дублем
|
|
@@ -87,14 +87,20 @@ export function useCategoryPage(
|
|
|
87
87
|
miniCardTitle: categoryInfo.value?.title,
|
|
88
88
|
} as ICardInfo));
|
|
89
89
|
|
|
90
|
+
const relatedSearchLimit = useAppConfig().categoryRelatedSearchLimit;
|
|
91
|
+
|
|
90
92
|
const relatedSearch = computed<IChipsItem[]>(() => {
|
|
91
93
|
if (DISALLOWED_SLUGS.some(item => slug.value.includes(item))) return [];
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
const items = [...(relatedData.value?.items || [])]
|
|
96
|
+
.sort((a, b) => Number(b.score) - Number(a.score))
|
|
97
|
+
.slice(0, relatedSearchLimit ?? undefined);
|
|
98
|
+
|
|
99
|
+
return items.map(item => ({
|
|
94
100
|
title: item.query,
|
|
95
101
|
value: item.query,
|
|
96
102
|
prefix: 'search',
|
|
97
|
-
}))
|
|
103
|
+
}));
|
|
98
104
|
});
|
|
99
105
|
|
|
100
106
|
const relatedCategories = computed(() => categoryInfo.value?.related);
|
package/i18n/locales/de-DE.json
CHANGED
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"show_all_models": "Alle anzeigen",
|
|
129
129
|
"all_categories": "Alphabetisch A-Z",
|
|
130
130
|
"related_searches": "Ähnliche Suchen",
|
|
131
|
+
"tags": "Tags",
|
|
131
132
|
"related_models": "Ähnliche Models",
|
|
132
133
|
"other_names": "Andere Namen",
|
|
133
134
|
"popular_models": "Beliebte Models",
|
package/i18n/locales/en-US.json
CHANGED
|
@@ -207,6 +207,7 @@
|
|
|
207
207
|
"show_all_models": "Show all",
|
|
208
208
|
"all_categories": "Alphabetical A-Z",
|
|
209
209
|
"related_searches": "Related searches",
|
|
210
|
+
"tags": "Tags",
|
|
210
211
|
"related_models": "Related models",
|
|
211
212
|
"other_names": "Other names",
|
|
212
213
|
"popular_models": "Popular models",
|
package/i18n/locales/es-ES.json
CHANGED
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"show_all_models": "Ver todos",
|
|
129
129
|
"all_categories": "Alfabético A-Z",
|
|
130
130
|
"related_searches": "Búsquedas relacionadas",
|
|
131
|
+
"tags": "Etiquetas",
|
|
131
132
|
"related_models": "Modelos relacionados",
|
|
132
133
|
"other_names": "Otros nombres",
|
|
133
134
|
"popular_models": "Modelos populares",
|
package/i18n/locales/fr-FR.json
CHANGED
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"show_all_models": "Voir tout",
|
|
129
129
|
"all_categories": "Alphabétique A-Z",
|
|
130
130
|
"related_searches": "Recherches associées",
|
|
131
|
+
"tags": "Tags",
|
|
131
132
|
"related_models": "Modèles associés",
|
|
132
133
|
"other_names": "Autres noms",
|
|
133
134
|
"popular_models": "Modèles populaires",
|
package/i18n/locales/it-IT.json
CHANGED
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"show_all_models": "Vedi tutti",
|
|
129
129
|
"all_categories": "Alfabetico A-Z",
|
|
130
130
|
"related_searches": "Ricerche correlate",
|
|
131
|
+
"tags": "Tag",
|
|
131
132
|
"related_models": "Modelli correlati",
|
|
132
133
|
"other_names": "Altri nomi",
|
|
133
134
|
"popular_models": "Modelli popolari",
|
package/i18n/locales/ja-JP.json
CHANGED
package/i18n/locales/ko-KR.json
CHANGED
package/i18n/locales/ru-RU.json
CHANGED
|
@@ -149,6 +149,7 @@
|
|
|
149
149
|
"show_all_models": "Показать все",
|
|
150
150
|
"all_categories": "По алфавиту А-Я",
|
|
151
151
|
"related_searches": "Похожие запросы",
|
|
152
|
+
"tags": "Теги",
|
|
152
153
|
"related_models": "Похожие модели",
|
|
153
154
|
"other_names": "Другие имена",
|
|
154
155
|
"popular_models": "Популярные модели",
|
package/i18n/locales/zh-CN.json
CHANGED
package/package.json
CHANGED
|
@@ -172,35 +172,29 @@ describe('cleanVideoData', () => {
|
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
const raw = (
|
|
175
|
+
const raw = (entities: unknown) => ({ tags: ['anal'], entities } as unknown as IRawVideoData);
|
|
176
176
|
|
|
177
|
-
describe('cleanVideoData —
|
|
178
|
-
it('v1:
|
|
179
|
-
const data = cleanVideoData(raw(
|
|
180
|
-
expect(data.tags).toEqual(['
|
|
177
|
+
describe('cleanVideoData — группы под плеером', () => {
|
|
178
|
+
it('v1: групп нет, теги — как пришли', () => {
|
|
179
|
+
const data = cleanVideoData(raw(undefined));
|
|
180
|
+
expect(data.tags).toEqual(['anal']);
|
|
181
181
|
expect(data.tagGroups).toEqual([]);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it('v2: группы по order, элементы внутри по order, пустые и неизвестного типа
|
|
184
|
+
it('v2: группы по order, элементы внутри по order, пустые и неизвестного типа выкинуты', () => {
|
|
185
185
|
const data = cleanVideoData(raw([
|
|
186
186
|
{ type: 'category', name: 'positions', title: 'Positions', order: 1, items: [{ name: 'doggy', title: 'Doggy style' }] },
|
|
187
187
|
{ type: 'category', name: 'empty', title: 'Empty', order: 2, items: [] },
|
|
188
188
|
{ type: 'unknown', name: 'odd', title: 'Odd', order: 3, items: [{ name: 'odd', title: 'Odd' }] },
|
|
189
|
-
{ type: 'tag', name: '
|
|
190
|
-
{ name: 'rimming', title: '
|
|
191
|
-
{ name: 'anal', title: '
|
|
192
|
-
{ name: 'doggy', title: '
|
|
189
|
+
{ type: 'tag', name: 'tag', title: 'tag', order: 0, items: [
|
|
190
|
+
{ name: 'rimming', title: 'rimming', order: 2 },
|
|
191
|
+
{ name: 'anal', title: 'anal', order: 1 },
|
|
192
|
+
{ name: 'doggy', title: 'doggy', order: 3 },
|
|
193
193
|
] },
|
|
194
194
|
]));
|
|
195
195
|
|
|
196
|
-
expect(data.tagGroups.map(group => group.name)).toEqual(['
|
|
197
|
-
expect(data.tagGroups[0].type).toBe('tag');
|
|
196
|
+
expect(data.tagGroups.map(group => group.name)).toEqual(['tag', 'positions']);
|
|
198
197
|
expect(data.tagGroups[0].items.map(item => item.name)).toEqual(['anal', 'rimming', 'doggy']);
|
|
199
|
-
expect(data.tags).toEqual(['
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('нет тегов — пусто', () => {
|
|
203
|
-
expect(cleanVideoData(raw(undefined)).tagGroups).toEqual([]);
|
|
204
|
-
expect(cleanVideoData(raw(undefined)).tags).toEqual([]);
|
|
198
|
+
expect(data.tags).toEqual(['anal']);
|
|
205
199
|
});
|
|
206
200
|
});
|
|
@@ -68,26 +68,19 @@ const byOrder = <T extends { order?: number }>(a: T, b: T) => (a.order ?? 0) - (
|
|
|
68
68
|
|
|
69
69
|
const TAG_GROUP_TYPES = new Set<string>(['tag', 'category', 'search']);
|
|
70
70
|
|
|
71
|
-
// v2 отдаёт в `
|
|
72
|
-
// пустые и неизвестного типа (ссылку не построить)
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
items: group.items.filter(item => item?.title && item.name).sort(byOrder),
|
|
85
|
-
}))
|
|
86
|
-
.filter(group => group.items.length > 0)
|
|
87
|
-
.sort(byOrder);
|
|
88
|
-
|
|
89
|
-
return { tags: [...new Set(tagGroups.flatMap(group => group.items.map(item => item.title)))], tagGroups };
|
|
90
|
-
};
|
|
71
|
+
// v2 отдаёт группы в `entities`. Группы сортируются по `order` (и элементы внутри, если он задан);
|
|
72
|
+
// пустые и неизвестного типа (ссылку не построить) отбрасываются.
|
|
73
|
+
const cleanTagGroups = (raw: IRawVideoData['entities']): IVideoTagGroup[] => (Array.isArray(raw) ? raw : [])
|
|
74
|
+
.filter(group => group?.title && TAG_GROUP_TYPES.has(group.type) && Array.isArray(group.items))
|
|
75
|
+
.map(group => ({
|
|
76
|
+
type: group.type,
|
|
77
|
+
name: group.name || '',
|
|
78
|
+
title: group.title,
|
|
79
|
+
order: group.order ?? 0,
|
|
80
|
+
items: group.items.filter(item => item?.title && item.name).sort(byOrder),
|
|
81
|
+
}))
|
|
82
|
+
.filter(group => group.items.length > 0)
|
|
83
|
+
.sort(byOrder);
|
|
91
84
|
|
|
92
85
|
/** Нормализует сырые данные видео к типу IVideoData. */
|
|
93
86
|
export const cleanVideoData = (data: IRawVideoData, theme?: string): IVideoData => ({
|
|
@@ -99,7 +92,8 @@ export const cleanVideoData = (data: IRawVideoData, theme?: string): IVideoData
|
|
|
99
92
|
views: data.views || 0,
|
|
100
93
|
likes: data.likes || 0,
|
|
101
94
|
id: data.id || '',
|
|
102
|
-
|
|
95
|
+
tags: data.tags || [],
|
|
96
|
+
tagGroups: cleanTagGroups(data.entities),
|
|
103
97
|
models: cleanVideoModels(data.models, theme),
|
|
104
98
|
categories: data.categories || [],
|
|
105
99
|
actions: data.actions || null,
|
|
@@ -13,8 +13,8 @@ export default defineEventHandler(async (event): Promise<IVideoData> => {
|
|
|
13
13
|
'user-agent': requestHeaders[ 'user-agent' ] ?? '',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
const videoUrl = getUrlWithProxiedParams(`${apiUrl}/
|
|
16
|
+
// v2 ищет ролик по `url` — это id или слаг, бекенд разбирает сам; группы под плеером — в `entities`
|
|
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
20
|
const [videoRaw, modelsRaw] = await Promise.all([
|
|
@@ -25,7 +25,7 @@ export default defineEventHandler(async (event): Promise<IVideoData> => {
|
|
|
25
25
|
apiFetch(modelsUrl, { method: 'GET', headers: clientHeaders }).catch(() => null),
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
|
-
const videoData =
|
|
28
|
+
const videoData = withDemoFallbacks(videoRaw.data as IRawVideoData);
|
|
29
29
|
const modelsData = modelsRaw?.data as Record<'items', Array<IRawModelCard>>;
|
|
30
30
|
|
|
31
31
|
return cleanVideoData(
|
|
@@ -12,8 +12,7 @@ export default defineEventHandler(async (event): Promise<IVideoData> => {
|
|
|
12
12
|
...requestHeaders,
|
|
13
13
|
'user-agent': requestHeaders[ 'user-agent' ] ?? '',
|
|
14
14
|
};
|
|
15
|
-
|
|
16
|
-
const videoUrl = getUrlWithProxiedParams(`${apiUrl}/v1/video/url/${url}`, event);
|
|
15
|
+
const videoUrl = getUrlWithProxiedParams(`${apiUrl}/v2/video/url/${url}`, event);
|
|
17
16
|
const modelsUrl = getUrlWithProxiedParams(`${apiUrl}/v2/models/list-by-video-url/${url}`, event);
|
|
18
17
|
|
|
19
18
|
const [videoRaw, modelsRaw] = await Promise.all([
|
|
@@ -24,7 +23,7 @@ export default defineEventHandler(async (event): Promise<IVideoData> => {
|
|
|
24
23
|
apiFetch(modelsUrl, { method: 'GET', headers: clientHeaders }).catch(() => null),
|
|
25
24
|
]);
|
|
26
25
|
|
|
27
|
-
const videoData =
|
|
26
|
+
const videoData = withDemoFallbacks(videoRaw.data as IRawVideoData);
|
|
28
27
|
const modelsData = modelsRaw?.data as Record<'items', Array<IRawModelCard>>;
|
|
29
28
|
|
|
30
29
|
return cleanVideoData(
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IRawVideoData } from '../../types';
|
|
2
|
+
|
|
3
|
+
// заглушка нарочно lorem — сразу видно, что не настоящее описание
|
|
4
|
+
const MOCK_DESCRIPTION = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et '
|
|
5
|
+
+ 'dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo '
|
|
6
|
+
+ 'consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* shortcut: демо блока под плеером, пока у роликов нет описаний и в админке не заведены разделы категорий — без описания
|
|
10
|
+
* подставляется lorem, без разделов категории ролика идут одной группой «Categories». Удалить, когда данные появятся.
|
|
11
|
+
*/
|
|
12
|
+
export function withDemoFallbacks(video: IRawVideoData): IRawVideoData {
|
|
13
|
+
if (!video) return video;
|
|
14
|
+
|
|
15
|
+
const entities = video.entities ?? [];
|
|
16
|
+
const hasSections = entities.some(group => group.type === 'category');
|
|
17
|
+
const categories = (video.categories ?? []).map(({ name, title, icon }) => ({ name, title, icon }));
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
...video,
|
|
21
|
+
description: video.description || MOCK_DESCRIPTION,
|
|
22
|
+
entities: hasSections || !categories.length
|
|
23
|
+
? entities
|
|
24
|
+
: [{ type: 'category' as const, name: 'categories', title: 'Categories', order: -1, items: categories }, ...entities],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -18,8 +18,9 @@ export interface IRawVideoData {
|
|
|
18
18
|
dislikes: number;
|
|
19
19
|
popularity: number; // unused
|
|
20
20
|
id: string;
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
tags: string[];
|
|
22
|
+
/** Только v2: категории по разделам, затем теги и поисковые фразы (`tag`/`search` — с `title`, равным типу). */
|
|
23
|
+
entities?: IVideoTagGroup[];
|
|
23
24
|
models: {
|
|
24
25
|
guid: string;
|
|
25
26
|
title: string;
|
package/types/video-data.d.ts
CHANGED
|
@@ -66,7 +66,6 @@ export interface IVideoData {
|
|
|
66
66
|
likes: number;
|
|
67
67
|
dislikes: number;
|
|
68
68
|
id: string;
|
|
69
|
-
/** Все теги одним списком: в v2 — склейка элементов всех групп. */
|
|
70
69
|
tags: string[];
|
|
71
70
|
/** Группы под плеером (раскрытое описание); у ответа v1 групп нет — пусто. */
|
|
72
71
|
tagGroups: IVideoTagGroup[];
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { IRawVideoData, IVideoTagGroup } from '../../types';
|
|
2
|
-
|
|
3
|
-
const item = (name: string) => ({ name, title: name.charAt(0).toUpperCase() + name.slice(1) });
|
|
4
|
-
const group = (order: number, name: string, title: string, names: string[], type: IVideoTagGroup['type'] = 'category'): IVideoTagGroup =>
|
|
5
|
-
({ type, name, title, order, items: names.map(item) });
|
|
6
|
-
|
|
7
|
-
/** Статичная заглушка групп (контракт ручки видео v2): настоящие категории, теги и поиски сайта (все три типа узлов),
|
|
8
|
-
* категорийных групп и элементов — как в макете. Одна на все ролики — только для отладки вида и поведения; группировать будет бекенд. */
|
|
9
|
-
const MOCK_TAG_GROUPS: IVideoTagGroup[] = [
|
|
10
|
-
group(0, 'actions', 'Actions', ['blowjob', 'cumshot', 'creampie', 'deep throat', 'handjob', 'fingering', 'squirt']),
|
|
11
|
-
group(1, 'positions', 'Positions', ['cowgirl', 'missionary', 'face sitting']),
|
|
12
|
-
group(2, 'body_type', 'Body type', ['big boobs', 'big ass', 'petite', 'big cock']),
|
|
13
|
-
group(3, 'appearance', 'Appearance', ['blond', 'brunette', 'redhead', 'tattoo']),
|
|
14
|
-
group(4, 'location', 'Location', ['bedroom']),
|
|
15
|
-
group(5, 'format', 'Format', ['amateur', 'point of view', 'compilation', 'homemade']),
|
|
16
|
-
group(6, 'tags', 'Tags', ['moaning', 'loud moaning', 'female orgasm', 'bouncing tits', 'natural big tits'], 'tag'),
|
|
17
|
-
group(7, 'searches', 'Related searches', ['amateur wife', 'big tits teen', 'homemade blowjob'], 'search'),
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
// заглушка нарочно lorem — сразу видно, что не настоящее описание
|
|
21
|
-
const MOCK_DESCRIPTION = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et '
|
|
22
|
-
+ 'dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo '
|
|
23
|
-
+ 'consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* shortcut: ручка видео v2 ещё не готова — BFF ходит в v1 и отдаёт вместо тегов статичную заглушку групп (и описание,
|
|
27
|
-
* если его нет: у роликов с группами оно будет всегда). Удалить вместе с переходом на `/v2/video/*`.
|
|
28
|
-
*/
|
|
29
|
-
export function withMockTagGroups(video: IRawVideoData): IRawVideoData {
|
|
30
|
-
if (!video) return video;
|
|
31
|
-
|
|
32
|
-
return { ...video, tags: MOCK_TAG_GROUPS, description: video.description || MOCK_DESCRIPTION };
|
|
33
|
-
}
|