Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,29 @@
.license {
margin-block: 1em;
}

.aiDisclosureSection {
margin-block-start: 1.5em;
}

.aiDisclosureTitle {
font-size: 20px;
font-weight: 600;
margin-block-end: 12px;
}

.aiDisclosureHeader {
font-weight: 600;
font-size: 15px;
margin-block-end: 4px;
}

.aiDisclosureBody {
font-size: 14px;
Comment thread
caetano-dev marked this conversation as resolved.
Outdated
line-height: 1.4;
opacity: 0.9;
}

.aiDisclosureItem + .aiDisclosureItem {
margin-block-start: 12px;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<FtCard
v-if="shownDescription.length > 0"
v-if="shownDescription.length > 0 || showHowThisWasMade"
:class="{ videoDescription: true, short: !showFullDescription }"
>
<span
Expand All @@ -14,13 +14,37 @@
{{ $t("Description.Expand Description") }}
</span>
<FtTimestampCatcher
v-if="shownDescription.length > 0"
ref="descriptionContainer"
class="description"
:input-html="processedShownDescription"
:link-tab-index="linkTabIndex"
@timestamp-event="onTimestamp"
@click="expandDescriptionWithClick"
/>
<div
v-if="showHowThisWasMade && showFullDescription"
class="aiDisclosureSection"
>
<div class="aiDisclosureTitle">
{{ contentDisclosures.title || t('Video.How this was made') }}
</div>
<div
v-for="(item, index) in contentDisclosures.items"
:key="index"
class="aiDisclosureItem"
>
<div class="aiDisclosureHeader">
{{ item.label }}
</div>
<div
v-if="item.description"
class="aiDisclosureBody"
>
{{ item.description }}
</div>
</div>
</div>
<bdi
v-if="license && showFullDescription"
class="license"
Expand All @@ -44,6 +68,7 @@
import autolinker from 'autolinker'

import { onMounted, ref, computed, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import FtCard from '../ft-card/ft-card.vue'
import FtTimestampCatcher from '../FtTimestampCatcher.vue'

Expand All @@ -59,9 +84,17 @@ const props = defineProps({
license: {
type: String,
default: null,
},
contentDisclosures: {
type: Object,
default: null
}
})

const { t } = useI18n()

const showHowThisWasMade = computed(() => (props.contentDisclosures?.items?.length ?? 0) > 0)

const emit = defineEmits(['timestamp-event'])

let shownDescription = ''
Expand Down Expand Up @@ -135,7 +168,10 @@ function collapseDescription() {
*/
function isShortDescription() {
const descriptionElem = descriptionContainer.value?.$el
return descriptionElem?.clientHeight >= descriptionElem?.scrollHeight
if (!descriptionElem) {
return true
Comment thread
Shadorc marked this conversation as resolved.
Outdated
}
return descriptionElem.clientHeight >= descriptionElem.scrollHeight
}

onMounted(() => {
Expand Down
14 changes: 12 additions & 2 deletions src/renderer/components/WatchVideoInfo/WatchVideoInfo.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,20 @@
gap: 10px;
}

.unlistedBadge {
.videoBadges {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-block-start: 4px;
}

.unlistedBadge,
.aiBadge {
background-color: var(--secondary-card-bg-color);
border-radius: 5px;
display: inline;
display: inline-flex;
align-items: center;
gap: 5px;
padding-inline: 5px;
padding-block: 2px;
white-space: nowrap;
Expand Down
28 changes: 25 additions & 3 deletions src/renderer/components/WatchVideoInfo/WatchVideoInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,24 @@
{{ title }}
</h1>
<div
v-if="isUnlisted"
class="unlistedBadge"
v-if="isUnlisted || showAiBadge"
class="videoBadges"
>
{{ t('Video.Unlisted') }}
<div
v-if="isUnlisted"
class="unlistedBadge"
>
{{ t('Video.Unlisted') }}
</div>
<div
v-if="showAiBadge"
class="aiBadge"
:title="aiTooltipText"
:aria-label="aiTooltipText"
>
<FontAwesomeIcon :icon="['fas', 'robot']" />
Comment thread
caetano-dev marked this conversation as resolved.
Outdated
<span>{{ t('Video.AI') }}</span>
</div>
</div>
</div>
<div class="videoMetrics">
Expand Down Expand Up @@ -231,6 +245,10 @@ const props = defineProps({
type: Boolean,
required: false
},
contentDisclosures: {
type: Object,
default: null
},
canSaveWatchedProgress: {
type: Boolean,
required: true
Expand All @@ -247,6 +265,10 @@ const USING_ELECTRON = process.env.IS_ELECTRON

const { locale, t } = useI18n()

const showAiBadge = computed(() => Boolean(props.contentDisclosures?.hasAI))
Comment thread
caetano-dev marked this conversation as resolved.
Outdated

const aiTooltipText = computed(() => props.contentDisclosures?.aiBadgeTooltip || 'AI: Content was made with AI')
Comment thread
caetano-dev marked this conversation as resolved.
Outdated

/** @type {import('vue').ComputedRef<boolean>} */
const hideSharingActions = computed(() => store.getters.getHideSharingActions)

Expand Down
65 changes: 64 additions & 1 deletion src/renderer/helpers/api/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ export async function getLocalVideoInfo(id) {

if ((info.playability_status.status === 'UNPLAYABLE' && (!hasTrailer || trailerIsAgeRestricted)) ||
info.playability_status.status === 'LOGIN_REQUIRED') {
return { info, poToken: undefined, clientInfo }
return { info, poToken: undefined, clientInfo, contentDisclosures: extractLocalContentDisclosures(info, nextResponse) }
}

if (hasTrailer && info.playability_status.status !== 'OK') {
Expand Down Expand Up @@ -767,6 +767,69 @@ export async function getLocalVideoInfo(id) {
poToken: contentPoToken,
clientInfo,
adEndTimeUnixMs,
contentDisclosures: extractLocalContentDisclosures(info, nextResponse),
}
}

/**
* Extracts AI badge tooltip from YouTube watch next response
* @param {object} nextResponse
* @returns {string|null}
*/
function extractAiBadgeTooltip(nextResponse) {
const data = nextResponse?.data ?? nextResponse
const contents = data?.contents?.twoColumnWatchNextResults?.results?.results?.contents
const primaryInfo = contents?.find(item => item?.videoPrimaryInfoRenderer)?.videoPrimaryInfoRenderer
const aiBadge = primaryInfo?.badges?.find(b => /\bai\b/i.test(b?.metadataBadgeRenderer?.label))

return aiBadge?.metadataBadgeRenderer?.accessibilityData?.label ?? null
}

/**
* Extracts content disclosures ('How this was made') from YouTube VideoInfo response
* @param {import('youtubei.js').YT.VideoInfo} info
* @param {object} [nextResponse]
* @returns {{ title: string|null, hasAI: boolean, aiBadgeTooltip: string|null, items: Array<{ label: string, description: string, isAI: boolean }> } | null}
*/
export function extractLocalContentDisclosures(info, nextResponse = null) {
Comment thread
Shadorc marked this conversation as resolved.
Outdated
if (!info) {
return null
}

const structuredDescPanel = info.page?.[1]?.engagement_panels?.find(
panel => panel.target_id === 'engagement-panel-structured-description'
)

const rawItems = structuredDescPanel?.content?.items?.filter(
item => item.type === 'HowThisWasMadeSectionView' || item.type === 'howThisWasMadeSectionViewModel'
)

const hasAiBadge = info.primary_info?.badges?.some(b => /\bai\b/i.test(b.label || '')) || false

if (!rawItems?.length && !hasAiBadge) {
return null
}

const items = (rawItems || []).map(item => {
const label = item.body_header?.text || item.bodyHeader?.content || ''
const rawDescription = item.body_text?.text || item.bodyText?.content || ''
return {
label,
description: rawDescription.replace(/\s*Learn more\.?$/i, '').trim(),
isAI: /\b(ai|artificial intelligence|altered|synthetic)\b/i.test(label)
}
})

const hasAI = hasAiBadge || items.some(item => item.isAI)
const aiBadgeTooltip = hasAI
? (extractAiBadgeTooltip(nextResponse) || 'AI: Content was made with AI')
: null

return {
title: rawItems?.[0]?.section_title?.text || rawItems?.[0]?.sectionTitle?.content || null,
hasAI,
aiBadgeTooltip,
items
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/renderer/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
faQuestionCircle,
faRandom,
faRetweet,
faRobot,
Comment thread
caetano-dev marked this conversation as resolved.
Outdated
faRss,
faSatelliteDish,
faSave,
Expand Down Expand Up @@ -215,6 +216,7 @@ library.add(
faQuestionCircle,
faRandom,
faRetweet,
faRobot,
Comment thread
caetano-dev marked this conversation as resolved.
Outdated
faRss,
faSatelliteDish,
faSave,
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/views/Watch/Watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export default defineComponent({
/** @type {string[]|null} */
customErrorIcon: null,
videoGenreIsMusic: false,
contentDisclosures: null,
/** @type {Date|null} */
streamingDataExpiryDate: null,
currentPlaybackRate: null,
Expand Down Expand Up @@ -400,6 +401,7 @@ export default defineComponent({
this.videoDescription = ''
this.videoDescriptionHtml = ''
this.license = ''
this.contentDisclosures = null
this.videoViewCount = 0
this.videoLikeCount = 0
this.videoDislikeCount = 0
Expand Down Expand Up @@ -502,7 +504,8 @@ export default defineComponent({

try {
const videoInfo = await getLocalVideoInfo(this.videoId)
const { info: result, poToken, clientInfo, adEndTimeUnixMs } = videoInfo
const { info: result, poToken, clientInfo, adEndTimeUnixMs, contentDisclosures } = videoInfo
this.contentDisclosures = contentDisclosures || null

const playabilityStatus = result.playability_status
this.playabilityStatus = playabilityStatus.status
Expand Down Expand Up @@ -1030,6 +1033,7 @@ export default defineComponent({
this.isFamilyFriendly = result.isFamilyFriendly
this.isPostLiveDvr = !!result.isPostLiveDvr
this.isUnlisted = !result.isListed
this.contentDisclosures = null

this.captions = sortCaptions(result.captions.map(caption => {
return {
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/views/Watch/Watch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
:video-thumbnail="thumbnail"
:in-user-playlist="!!selectedUserPlaylist"
:is-unlisted="isUnlisted"
:content-disclosures="contentDisclosures"
:can-save-watched-progress="canSaveWatchProgress"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
Expand All @@ -172,6 +173,7 @@
:description="videoDescription"
:description-html="videoDescriptionHtml"
:license="license"
:content-disclosures="contentDisclosures"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
@timestamp-event="changeTimestamp"
Expand Down
2 changes: 2 additions & 0 deletions static/locales/en-US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,8 @@ Video:
Premieres: Premieres
Upcoming: Upcoming
Unlisted: Unlisted
AI: AI
How this was made: How this was made
Station: Station
Live: Live
Live Now: Live Now
Expand Down