{"release_id":"01a1998d-9802-0000-b0f7-33a23307f4ed","chunk_id":"01a1998d-9956-7c60-8741-a56c297aac90","file":"j17DfC5y.mjs","mappings":"+VAgBO,MAAMA,EAAwB,GA2B9B,SAASC,EAAYC,EAAqB,CAChD,MAAM,EAAI,KAAK,IAAI,EAAG,KAAK,MAAMA,CAAG,CAAC,EAC/BC,EAAI,KAAK,MAAM,EAAI,IAAI,EACvBC,EAAI,KAAK,MAAO,EAAI,KAAQ,EAAE,EAC9BC,EAAI,EAAI,GACd,MAAO,KAAKF,EAAI,GAAGA,CAAC,IAAM,EAAE,GAAGC,EAAI,GAAGA,CAAC,IAAM,EAAE,GAAGC,GAAM,CAACF,GAAK,CAACC,EAAK,GAAGC,CAAC,IAAM,EAAE,EACjF,CAGA,MAAMC,EAAqC,CAC1C,CAAC,MAAO,KAAK,EACb,CAAC,WAAY,UAAU,EACvB,CAAC,MAAO,OAAO,EACf,CAAC,QAAS,iBAAiB,CAC5B,EAEO,SAASC,EAAYC,EAAgE,CAC3F,SAAW,CAACC,EAAKC,CAAI,IAAKJ,EAAc,CACvC,GAAI,CAACE,EAAK,SAASC,CAAG,EAAG,SACzB,MAAME,EAAOC,EAAU,KAAMC,GAAMA,EAAE,OAASH,CAAI,EAClD,GAAIC,QAAa,CAAE,KAAMG,EAAYH,CAAI,EAAG,KAAMA,EAAK,IAAA,CACxD,CACA,OAAO,IACR","names":["WATCH_MIN_DESCRIPTION","isoDuration","sec","h","m","r","SPOKE_BY_TAG","sphSpokeFor","tags","tag","slug","page","SPH_PAGES","p","sphPagePath"],"sources":["../../../../../../src/lib/album-watch.ts"],"sourcesContent":["import { SPH_PAGES, sphPagePath } from '$lib/sph/pages';\nimport type { SphTopic } from '$lib/sph/types';\n\n/**\n * An album page as a video watch page (RAT-618). Google shows a video in its\n * video results only from a page where the video is the main content, so an\n * album whose free preview is a clip gets that clip as a player at the top.\n * Of those, the ones with something to say get indexed: a real title, a\n * description of at least WATCH_MIN_DESCRIPTION characters, and nothing that\n * reads as a minor or as family (listing-risk.ts). The rest stay noindex.\n * 18 and 19 are adult ages: an album that says so is indexed like any other.\n *\n * On 2026-10-25: 768 published albums, 296 with a clip, 51 with a clip as a\n * free preview, 12 of those indexable.\n */\n\nexport const WATCH_MIN_DESCRIPTION = 80;\n\nexport type WatchCandidate = {\n\tstatus: string;\n\ttitle: string;\n\tdescription: string | null;\n\thasPreviewVideo: boolean;\n\t/** title, description or nickname reads as a minor or as family */\n\trisky: boolean;\n\t/** the creator can't sell (banned, suspended) */\n\toffShelf: boolean;\n};\n\nconst UNTITLED = new Set(['untitled album', 'untitled', '']);\n\nexport function isWatchPage(a: WatchCandidate): boolean {\n\treturn (\n\t\ta.status === 'published' &&\n\t\t!a.offShelf &&\n\t\t!a.risky &&\n\t\ta.hasPreviewVideo &&\n\t\t!UNTITLED.has(a.title.trim().toLowerCase()) &&\n\t\t(a.description?.trim().length ?? 0) >= WATCH_MIN_DESCRIPTION\n\t);\n}\n\n/** ISO 8601 duration for VideoObject: 604 → \"PT10M4S\". */\nexport function isoDuration(sec: number): string {\n\tconst s = Math.max(0, Math.round(sec));\n\tconst h = Math.floor(s / 3600);\n\tconst m = Math.floor((s % 3600) / 60);\n\tconst r = s % 60;\n\treturn `PT${h ? `${h}H` : ''}${m ? `${m}M` : ''}${r || (!h && !m) ? `${r}S` : ''}`;\n}\n\n/** The /sph page an album's tags belong on, most specific first. */\nconst SPOKE_BY_TAG: [string, SphTopic][] = [\n\t['joi', 'joi'],\n\t['captions', 'captions'],\n\t['sph', 'clips'],\n\t['small', 'small-dick-pics']\n];\n\nexport function sphSpokeFor(tags: readonly string[]): { path: string; name: string } | null {\n\tfor (const [tag, slug] of SPOKE_BY_TAG) {\n\t\tif (!tags.includes(tag)) continue;\n\t\tconst page = SPH_PAGES.find((p) => p.slug === slug);\n\t\tif (page) return { path: sphPagePath(page), name: page.name };\n\t}\n\treturn null;\n}\n\n/** What an album is, for its title: \"SPH JOI Clip\", \"Video Album\". */\nfunction albumKind(tags: readonly string[]): string {\n\tif (tags.includes('joi')) return 'SPH JOI Clip';\n\tif (tags.includes('captions')) return 'SPH Caption Set';\n\tif (tags.includes('sph')) return 'SPH Clip';\n\treturn 'Video Album';\n}\n\n/**\n * The search title of a watch page: \"Gooning Loser — SPH JOI Clip (AI-Made,\n * 10 min) | RateMyD\". The parenthesis goes first when it would run long, then\n * the kind.\n */\nexport function watchPageTitle(\n\ttitle: string,\n\ttags: readonly string[],\n\tminutes: number | null\n): string {\n\tconst bits = [tags.includes('ai') ? 'AI-Made' : null, minutes ? `${minutes} min` : null].filter(\n\t\tBoolean\n\t);\n\tconst name = title.trim();\n\tconst kind = albumKind(tags);\n\tconst options = [\n\t\tbits.length ? `${name} — ${kind} (${bits.join(', ')}) | RateMyD` : null,\n\t\t`${name} — ${kind} | RateMyD`,\n\t\t`${name} | RateMyD`\n\t].filter((t): t is string => !!t);\n\treturn options.find((t) => t.length <= 70) ?? options[options.length - 1];\n}\n\n/** The meta description: the album's own words, cut at a word under 160. */\nexport function watchPageDescription(description: string): string {\n\tconst d = description.trim().replace(/\\s+/g, ' ');\n\tif (d.length <= 160) return d;\n\tconst cut = d.slice(0, 157);\n\treturn `${cut.slice(0, cut.lastIndexOf(' '))}…`;\n}\n"],"version":3}