import { VIDEO_POOL } from './videos'; import type { StubVideo } from './videos'; import { PlaylistType } from '../runtime/constants/playlist-type'; import type { IPlaylistCard } from '../types'; const HOUR = 3600; const DAY = 24 * HOUR; /** Сколько последних роликов сущности показываем в ленте. */ const PREVIEW_LIMIT = 5; export type LibraryFollowVideo = StubVideo & { guid: string; isNew: boolean; }; type LibraryFollowBase = { guid: string; title: string; videosCount: number; /** Сколько роликов вышло с прошлого захода; 0 — плашки «новое» нет */ newCount: number; /** Unix timestamp (сек.) последнего добавленного ролика */ lastVideoAddedAt: number; videos: LibraryFollowVideo[]; }; export type LibraryModelStub = LibraryFollowBase & { avatarUrl: string; country: { name: string title: string }; age: number; /** Параметры модели из API: guy_type, body_build, ethnicity */ guyType: string; bodyBuild: string; ethnicity: string; }; export type LibraryChannelStub = LibraryFollowBase & { avatarUrl?: string; followersCount: number; tags: string[]; }; export type LibraryCategoryStub = LibraryFollowBase; /** Плейлист подписки: карточка как в общем гриде + число новых роликов. */ export type LibraryPlaylistStub = IPlaylistCard & { newCount: number }; const now = () => Math.floor(Date.now() / 1000); const toSlug = (title: string) => title.toLowerCase().replace(/\s+/g, '-'); /** Нарезает пул на ленту сущности: guid уникален внутри неё, первые `newCount` помечены новинками. */ function buildVideos(slug: string, offset: number, newCount: number): LibraryFollowVideo[] { return Array.from({ length: PREVIEW_LIMIT }, (_, index) => { const video = VIDEO_POOL[(offset + index) % VIDEO_POOL.length]; return { ...video, guid: `${slug}-${video.id}`, isNew: index < newCount, }; }); } /** Порядок по числу новинок намеренно не совпадает с алфавитным — иначе обе сортировки дают одно и то же. */ const MODELS = [ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 }, { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, guyType: 'Daddy', bodyBuild: 'Muscular', ethnicity: 'Latino', updatedAgo: 2 * DAY, videoOffset: 9 }, { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, guyType: 'Twink', bodyBuild: 'Athletic', ethnicity: 'Caucasian', updatedAgo: 9 * DAY, videoOffset: 0 }, { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, guyType: 'Jock', bodyBuild: 'Muscular', ethnicity: 'Black', updatedAgo: 21 * DAY, videoOffset: 6 }, { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 }, { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, guyType: 'Daddy', bodyBuild: 'Muscular', ethnicity: 'Latino', updatedAgo: 2 * DAY, videoOffset: 9 }, { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, guyType: 'Twink', bodyBuild: 'Athletic', ethnicity: 'Caucasian', updatedAgo: 9 * DAY, videoOffset: 0 }, { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, guyType: 'Jock', bodyBuild: 'Muscular', ethnicity: 'Black', updatedAgo: 21 * DAY, videoOffset: 6 }, { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 }, { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 }, { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 }, { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 }, { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, guyType: 'Daddy', bodyBuild: 'Muscular', ethnicity: 'Latino', updatedAgo: 2 * DAY, videoOffset: 9 }, { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, guyType: 'Twink', bodyBuild: 'Athletic', ethnicity: 'Caucasian', updatedAgo: 9 * DAY, videoOffset: 0 }, { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, guyType: 'Jock', bodyBuild: 'Muscular', ethnicity: 'Black', updatedAgo: 21 * DAY, videoOffset: 6 }, ]; const CHANNELS = [ { title: 'Top To Bottom', avatarUrl: undefined, videosCount: 17, followersCount: 12800, newCount: 3, tags: ['Versatile', 'Flip-Flop', 'HD', 'Professional', 'Exclusive', 'Bareback'], updatedAgo: 4 * HOUR, videoOffset: 2 }, { title: 'Icon Male', avatarUrl: 'https://nli01.joystorage.com/c/0/2/c02a3f8ef14943b5f3da537aaed46fcc.jpeg', videosCount: 128, followersCount: 128400, newCount: 2, tags: ['Muscle', 'Daddy', 'Bareback', 'Professional', 'Exclusive', 'HD', 'Studio'], updatedAgo: DAY, videoOffset: 0 }, { title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 }, { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 }, { title: 'Top To Bottom', avatarUrl: undefined, videosCount: 17, followersCount: 12800, newCount: 3, tags: ['Versatile', 'Flip-Flop', 'HD', 'Professional', 'Exclusive', 'Bareback'], updatedAgo: 4 * HOUR, videoOffset: 2 }, { title: 'Icon Male', avatarUrl: 'https://nli01.joystorage.com/c/0/2/c02a3f8ef14943b5f3da537aaed46fcc.jpeg', videosCount: 128, followersCount: 128400, newCount: 2, tags: ['Muscle', 'Daddy', 'Bareback', 'Professional', 'Exclusive', 'HD', 'Studio'], updatedAgo: DAY, videoOffset: 0 }, { title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 }, { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 }, { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 }, { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 }, { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 }, { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 }, { title: 'Top To Bottom', avatarUrl: undefined, videosCount: 17, followersCount: 12800, newCount: 3, tags: ['Versatile', 'Flip-Flop', 'HD', 'Professional', 'Exclusive', 'Bareback'], updatedAgo: 4 * HOUR, videoOffset: 2 }, { title: 'Icon Male', avatarUrl: 'https://nli01.joystorage.com/c/0/2/c02a3f8ef14943b5f3da537aaed46fcc.jpeg', videosCount: 128, followersCount: 128400, newCount: 2, tags: ['Muscle', 'Daddy', 'Bareback', 'Professional', 'Exclusive', 'HD', 'Studio'], updatedAgo: DAY, videoOffset: 0 }, { title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 }, { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 }, ]; /** * Широкие категории пополняются постоянно, поэтому новинки у них есть всегда; узкие вроде Firemen * неделями стоят на месте — там `newCount: 0`, и лента откатывается на лучшее за неделю. */ const CATEGORIES = [ { title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 }, { title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 }, { title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 }, { title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 }, { title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 }, { title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 }, ]; /** Чужие плейлисты, на которые подписан пользователь. */ /** Порядок по дате намеренно не совпадает с алфавитным — иначе обе сортировки дают одно и то же. */ const PLAYLISTS = [ { name: 'Suits after office', username: 'andyblair2', videosCount: 24, likes: 156, views: 1200, newCount: 3, searchTags: ['suits', 'office', 'group'], createdAgo: 12 * DAY, videoOffset: 0 }, { name: 'Bears in the bathroom: steamy hairy hunks compilation vol. 3', username: 'gay.cool', videosCount: 48, likes: 2100, views: 89000, newCount: 2, searchTags: ['bears', 'bathroom', 'hairy'], createdAgo: 14 * DAY, videoOffset: 4 }, { name: 'Locker room legends: after-practice muscle jocks marathon', username: 'mike_d', videosCount: 32, likes: 892, views: 3400, newCount: 1, searchTags: ['jocks', 'locker', 'muscle'], createdAgo: 28 * DAY, videoOffset: 8 }, { name: 'Amateur couples first time', username: 'andyblair2', videosCount: 11, likes: 563, views: 234, newCount: 0, searchTags: ['amateur', 'couples', 'raw'], createdAgo: 25 * DAY, videoOffset: 2 }, { name: 'Poolside summer', username: 'gay.cool', videosCount: 15, likes: 1800, views: 76000, newCount: 0, searchTags: ['outdoor', 'twinks', 'summer'], createdAgo: 35 * DAY, videoOffset: 6 }, { name: 'Leather night', username: 'darkroom', videosCount: 9, likes: 45, views: 156, newCount: 0, searchTags: ['leather', 'fetish', 'kink'], createdAgo: 45 * DAY, videoOffset: 10 }, { name: 'Weekend binge', username: 'mattdvoe', videosCount: 33, likes: 271, views: 2400, newCount: 3, searchTags: ['bareback', 'compilation', 'hd'], createdAgo: 5 * DAY, videoOffset: 3 }, ]; /** * Подписки пользователя. Ручек под них пока нет — вёрстка живёт на заглушке. * Возраст ролика считаем от «сейчас», чтобы даты в разметке не устаревали. */ export function getLibraryModelsStub(): LibraryModelStub[] { const stamp = now(); return MODELS.map(({ image, updatedAgo, videoOffset, ...model }) => { const guid = toSlug(model.title); return { ...model, guid, avatarUrl: `/img/models/${image}`, lastVideoAddedAt: stamp - updatedAgo, videos: buildVideos(guid, videoOffset, model.newCount), }; }); } export function getLibraryChannelsStub(): LibraryChannelStub[] { const stamp = now(); return CHANNELS.map(({ updatedAgo, videoOffset, ...channel }) => { const guid = toSlug(channel.title); return { ...channel, guid, lastVideoAddedAt: stamp - updatedAgo, videos: buildVideos(guid, videoOffset, channel.newCount), }; }); } /** Мозаика обложек плейлиста: карточка раскладывает до 4 тумб. */ function buildThumbs(offset: number, count: number) { return Array.from({ length: Math.min(count, 4) }, (_, index) => ( VIDEO_POOL[(offset + index) % VIDEO_POOL.length].thumbUrls )); } export function getLibraryPlaylistsStub(): LibraryPlaylistStub[] { const stamp = now(); return PLAYLISTS.map(({ createdAgo, videoOffset, ...playlist }, index) => ({ ...playlist, id: `${index + 1}`, playlistType: PlaylistType.Public, created: stamp - createdAgo, firstVideoId: VIDEO_POOL[videoOffset % VIDEO_POOL.length].id, thumbUrls: buildThumbs(videoOffset, playlist.videosCount), })); } /** Сумма новинок по всем подпискам — счётчик на колокольчике и в шапках секций. */ export function getFollowingUpdatesTotal(): number { return [...MODELS, ...CHANNELS, ...CATEGORIES, ...PLAYLISTS] .reduce((total, item) => total + item.newCount, 0); } export function getLibraryCategoriesStub(): LibraryCategoryStub[] { const stamp = now(); return CATEGORIES.map(({ updatedAgo, videoOffset, ...category }) => { const guid = toSlug(category.title); return { ...category, guid, lastVideoAddedAt: stamp - updatedAgo, videos: buildVideos(guid, videoOffset, category.newCount), }; }); }