mirror of
https://github.com/FreeTubeApp/FreeTube
synced 2025-01-05 17:15:12 +01:00
Migrate comments to YouTube.js (#3072)
* Migrate comments to YouTube.js * Various improvements
This commit is contained in:
parent
cb64004080
commit
0145a0425f
@ -55,7 +55,6 @@
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||
"@freetube/yt-comment-scraper": "^6.2.0",
|
||||
"@silvermine/videojs-quality-selector": "^1.2.5",
|
||||
"autolinker": "^4.0.0",
|
||||
"browserify": "^17.0.0",
|
||||
|
@ -3,15 +3,9 @@ import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||
import FtSelect from '../../components/ft-select/ft-select.vue'
|
||||
import FtTimestampCatcher from '../../components/ft-timestamp-catcher/ft-timestamp-catcher.vue'
|
||||
import autolinker from 'autolinker'
|
||||
import ytcm from '@freetube/yt-comment-scraper'
|
||||
import {
|
||||
copyToClipboard,
|
||||
showToast,
|
||||
stripHTML,
|
||||
toLocalePublicationString
|
||||
} from '../../helpers/utils'
|
||||
import { copyToClipboard, showToast } from '../../helpers/utils'
|
||||
import { invidiousGetCommentReplies, invidiousGetComments } from '../../helpers/api/invidious'
|
||||
import { getLocalComments, parseLocalComment } from '../../helpers/api/local'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WatchVideoComments',
|
||||
@ -39,12 +33,9 @@ export default defineComponent({
|
||||
return {
|
||||
isLoading: false,
|
||||
showComments: false,
|
||||
commentScraper: null,
|
||||
nextPageToken: null,
|
||||
commentData: [],
|
||||
sortNewest: false,
|
||||
commentProcess: null,
|
||||
sortingChanged: false
|
||||
sortNewest: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -82,52 +73,23 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy: function () {
|
||||
if (this.commentProcess !== null) {
|
||||
this.commentProcess.send('end')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTimestamp: function (timestamp) {
|
||||
this.$emit('timestamp-event', timestamp)
|
||||
},
|
||||
|
||||
handleSortChange: function (sortType) {
|
||||
handleSortChange: function () {
|
||||
this.sortNewest = !this.sortNewest
|
||||
switch (this.backendPreference) {
|
||||
case 'local':
|
||||
this.isLoading = true
|
||||
this.commentData = []
|
||||
this.nextPageToken = undefined
|
||||
this.getCommentDataLocal({
|
||||
videoId: this.id,
|
||||
setCookie: false,
|
||||
sortByNewest: this.sortNewest,
|
||||
continuation: this.nextPageToken ? this.nextPageToken : undefined
|
||||
})
|
||||
break
|
||||
case 'invidious':
|
||||
this.isLoading = true
|
||||
this.commentData = []
|
||||
this.getCommentDataInvidious()
|
||||
break
|
||||
}
|
||||
this.commentData = []
|
||||
this.getCommentData()
|
||||
},
|
||||
|
||||
getCommentData: function () {
|
||||
this.isLoading = true
|
||||
switch (this.backendPreference) {
|
||||
case 'local':
|
||||
this.getCommentDataLocal({
|
||||
videoId: this.id,
|
||||
setCookie: false,
|
||||
sortByNewest: this.sortNewest,
|
||||
continuation: this.nextPageToken ? this.nextPageToken : undefined
|
||||
})
|
||||
break
|
||||
case 'invidious':
|
||||
this.getCommentDataInvidious()
|
||||
break
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
this.getCommentDataInvidious()
|
||||
} else {
|
||||
this.getCommentDataLocal()
|
||||
}
|
||||
},
|
||||
|
||||
@ -135,7 +97,11 @@ export default defineComponent({
|
||||
if (this.commentData.length === 0 || this.nextPageToken === null || typeof this.nextPageToken === 'undefined') {
|
||||
showToast(this.$t('Comments.There are no more comments for this video'))
|
||||
} else {
|
||||
this.getCommentData()
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
this.getCommentDataInvidious()
|
||||
} else {
|
||||
this.getCommentDataLocal(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -148,100 +114,87 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
getCommentReplies: function (index) {
|
||||
switch (this.commentData[index].dataType) {
|
||||
case 'local':
|
||||
this.getCommentRepliesLocal({
|
||||
videoId: this.id,
|
||||
setCookie: false,
|
||||
sortByNewest: this.sortNewest,
|
||||
replyToken: this.commentData[index].replyToken,
|
||||
index: index
|
||||
})
|
||||
break
|
||||
case 'invidious':
|
||||
this.getCommentRepliesInvidious(index)
|
||||
break
|
||||
if (process.env.IS_ELECTRON) {
|
||||
switch (this.commentData[index].dataType) {
|
||||
case 'local':
|
||||
this.getCommentRepliesLocal(index)
|
||||
break
|
||||
case 'invidious':
|
||||
this.getCommentRepliesInvidious(index)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
this.getCommentRepliesInvidious(index)
|
||||
}
|
||||
},
|
||||
|
||||
getCommentDataLocal: function (payload) {
|
||||
ytcm.getComments(payload).then((response) => {
|
||||
this.parseLocalCommentData(response, null)
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (this.backendFallback && this.backendPreference === 'local') {
|
||||
showToast(this.$t('Falling back to Invidious API'))
|
||||
this.getCommentDataInvidious()
|
||||
getCommentDataLocal: async function (more) {
|
||||
try {
|
||||
/** @type {import('youtubei.js/dist/src/parser/youtube/Comments').default} */
|
||||
let comments
|
||||
if (more) {
|
||||
comments = await this.nextPageToken.getContinuation()
|
||||
} else {
|
||||
this.isLoading = false
|
||||
comments = await getLocalComments(this.id, this.sortNewest)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getCommentRepliesLocal: function (payload) {
|
||||
showToast(this.$t('Comments.Getting comment replies, please wait'))
|
||||
const parsedComments = comments.contents
|
||||
.map(commentThread => parseLocalComment(commentThread.comment, commentThread))
|
||||
|
||||
ytcm.getCommentReplies(payload).then((response) => {
|
||||
this.parseLocalCommentData(response, payload.index)
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (this.backendFallback && this.backendPreference === 'local') {
|
||||
showToast(this.$t('Falling back to Invidious API'))
|
||||
this.getCommentDataInvidious()
|
||||
if (more) {
|
||||
this.commentData = this.commentData.concat(parsedComments)
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
parseLocalCommentData: function (response, index = null) {
|
||||
const commentData = response.comments.map((comment) => {
|
||||
comment.authorLink = comment.authorId
|
||||
comment.showReplies = false
|
||||
comment.authorThumb = comment.authorThumb[0].url
|
||||
comment.replies = []
|
||||
comment.dataType = 'local'
|
||||
comment.time = toLocalePublicationString({
|
||||
publishText: (comment.time + ' ago')
|
||||
})
|
||||
|
||||
if (this.hideCommentLikes) {
|
||||
comment.likes = null
|
||||
this.commentData = parsedComments
|
||||
}
|
||||
|
||||
comment.text = autolinker.link(stripHTML(comment.text))
|
||||
if (comment.customEmojis.length > 0) {
|
||||
comment.customEmojis.forEach(emoji => {
|
||||
comment.text = comment.text.replace(emoji.text, `<img width="14" height="14" class="commentCustomEmoji" alt="${emoji.text.substring(2, emoji.text.length - 1)}" src="${emoji.emojiThumbnails[0].url}">`)
|
||||
})
|
||||
}
|
||||
|
||||
return comment
|
||||
})
|
||||
|
||||
if (index !== null) {
|
||||
if (this.commentData[index].replies.length === 0 || this.commentData[index].replies[this.commentData[index].replies.length - 1].commentId !== commentData[commentData.length - 1].commentId) {
|
||||
this.commentData[index].replies = this.commentData[index].replies.concat(commentData)
|
||||
this.commentData[index].replyToken = response.continuation
|
||||
this.commentData[index].showReplies = true
|
||||
}
|
||||
} else {
|
||||
if (this.sortingChanged) {
|
||||
this.commentData = []
|
||||
this.sortingChanged = false
|
||||
}
|
||||
this.commentData = this.commentData.concat(commentData)
|
||||
this.nextPageToken = comments.has_continuation ? comments : null
|
||||
this.isLoading = false
|
||||
this.showComments = true
|
||||
this.nextPageToken = response.continuation
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (this.backendFallback && this.backendPreference === 'local') {
|
||||
showToast(this.$t('Falling back to Invidious API'))
|
||||
this.getCommentDataInvidious()
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getCommentRepliesLocal: async function (index) {
|
||||
showToast(this.$t('Comments.Getting comment replies, please wait'))
|
||||
|
||||
try {
|
||||
const comment = this.commentData[index]
|
||||
/** @type {import('youtubei.js/dist/src/parser/classes/comments/CommentThread').default} */
|
||||
const commentThread = comment.replyToken
|
||||
|
||||
if (comment.replies.length > 0) {
|
||||
await commentThread.getContinuation()
|
||||
comment.replies = comment.replies.concat(commentThread.replies.map(reply => parseLocalComment(reply)))
|
||||
} else {
|
||||
await commentThread.getReplies()
|
||||
comment.replies = commentThread.replies.map(reply => parseLocalComment(reply))
|
||||
}
|
||||
|
||||
comment.replyToken = commentThread.has_continuation ? commentThread : null
|
||||
comment.showReplies = true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (this.backendFallback && this.backendPreference === 'local') {
|
||||
showToast(this.$t('Falling back to Invidious API'))
|
||||
this.getCommentDataInvidious()
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -263,12 +216,7 @@ export default defineComponent({
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendFallback && this.backendPreference === 'invidious') {
|
||||
showToast(this.$t('Falling back to local API'))
|
||||
this.getCommentDataLocal({
|
||||
videoId: this.id,
|
||||
setCookie: false,
|
||||
sortByNewest: this.sortNewest,
|
||||
continuation: this.nextPageToken ? this.nextPageToken : undefined
|
||||
})
|
||||
this.getCommentDataLocal()
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
|
@ -104,11 +104,14 @@
|
||||
@timestamp-event="onTimestamp"
|
||||
/>
|
||||
<p class="commentLikeCount">
|
||||
<font-awesome-icon
|
||||
<template
|
||||
v-if="!hideCommentLikes"
|
||||
:icon="['fas', 'thumbs-up']"
|
||||
/>
|
||||
{{ comment.likes }}
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'thumbs-up']"
|
||||
/>
|
||||
{{ comment.likes }}
|
||||
</template>
|
||||
<span
|
||||
v-if="comment.isHearted"
|
||||
class="commentHeartBadge"
|
||||
@ -168,18 +171,15 @@
|
||||
>
|
||||
</router-link>
|
||||
<p class="commentAuthorWrapper">
|
||||
<span
|
||||
<router-link
|
||||
class="commentAuthor"
|
||||
:class="{
|
||||
commentOwner: reply.isOwner
|
||||
}"
|
||||
:to="`/channel/${reply.authorLink}`"
|
||||
>
|
||||
<router-link
|
||||
:to="`/channel/${reply.authorLink}`"
|
||||
>
|
||||
{{ reply.author }}
|
||||
</router-link>
|
||||
</span>
|
||||
{{ reply.author }}
|
||||
</router-link>
|
||||
<img
|
||||
v-if="reply.isMember"
|
||||
:src="reply.memberIconUrl"
|
||||
@ -196,11 +196,15 @@
|
||||
@timestamp-event="onTimestamp"
|
||||
/>
|
||||
<p class="commentLikeCount">
|
||||
<font-awesome-icon
|
||||
<template
|
||||
v-if="!hideCommentLikes"
|
||||
:icon="['fas', 'thumbs-up']"
|
||||
/>
|
||||
{{ reply.likes }}
|
||||
>
|
||||
<font-awesome-icon
|
||||
v-if="!hideCommentLikes"
|
||||
:icon="['fas', 'thumbs-up']"
|
||||
/>
|
||||
{{ reply.likes }}
|
||||
</template>
|
||||
</p>
|
||||
<p
|
||||
v-if="reply.numReplies > 0"
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { Innertube } from 'youtubei.js'
|
||||
import { ClientType } from 'youtubei.js/dist/src/core/Session'
|
||||
import EmojiRun from 'youtubei.js/dist/src/parser/classes/misc/EmojiRun'
|
||||
import Autolinker from 'autolinker'
|
||||
import { join } from 'path'
|
||||
|
||||
import { PlayerCache } from './PlayerCache'
|
||||
import { extractNumberFromString, getUserDataPath } from '../utils'
|
||||
import {
|
||||
extractNumberFromString,
|
||||
getUserDataPath,
|
||||
toLocalePublicationString
|
||||
} from '../utils'
|
||||
|
||||
/**
|
||||
* Creates a lightweight Innertube instance, which is faster to create or
|
||||
@ -141,6 +146,11 @@ export async function getLocalVideoInfo(id, attemptBypass = false) {
|
||||
return info
|
||||
}
|
||||
|
||||
export async function getLocalComments(id, sortByNewest = false) {
|
||||
const innertube = await createInnertube()
|
||||
return innertube.getComments(id, sortByNewest ? 'NEWEST_FIRST' : 'TOP_COMMENTS')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('youtubei.js/dist/src/parser/classes/misc/Format').default[]} formats
|
||||
* @param {import('youtubei.js/dist/index').Player} player
|
||||
@ -368,7 +378,7 @@ export function parseLocalTextRuns(runs, emojiSize = 16) {
|
||||
// that way we avoid layout shifts when it loads
|
||||
parsedRuns.push(`<img src="${emoji.image[0].url}" alt="${altText}" width="${emojiSize}" height="${emojiSize}" loading="lazy" style="vertical-align: middle">`)
|
||||
} else {
|
||||
const { text, endpoint } = run
|
||||
const { text, bold, italics, strikethrough, endpoint } = run
|
||||
|
||||
if (endpoint && !text.startsWith('#')) {
|
||||
switch (endpoint.metadata.page_type) {
|
||||
@ -379,13 +389,15 @@ export function parseLocalTextRuns(runs, emojiSize = 16) {
|
||||
parsedRuns.push(`https://www.youtube.com${endpoint.metadata.url}`)
|
||||
}
|
||||
break
|
||||
case 'WEB_PAGE_TYPE_CHANNEL':
|
||||
if (text.startsWith('@')) {
|
||||
parsedRuns.push(`<a href="https://www.youtube.com/channel/${endpoint.payload.browseId}">${text}</a>`)
|
||||
case 'WEB_PAGE_TYPE_CHANNEL': {
|
||||
const trimmedText = text.trim()
|
||||
if (trimmedText.startsWith('@')) {
|
||||
parsedRuns.push(`<a href="https://www.youtube.com/channel/${endpoint.payload.browseId}">${trimmedText}</a>`)
|
||||
} else {
|
||||
parsedRuns.push(`https://www.youtube.com${endpoint.metadata.url}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'WEB_PAGE_TYPE_PLAYLIST':
|
||||
parsedRuns.push(`https://www.youtube.com${endpoint.metadata.url}`)
|
||||
break
|
||||
@ -411,7 +423,20 @@ export function parseLocalTextRuns(runs, emojiSize = 16) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parsedRuns.push(text)
|
||||
let formattedText = text
|
||||
if (bold) {
|
||||
formattedText = `<b>${formattedText}</b>`
|
||||
}
|
||||
|
||||
if (italics) {
|
||||
formattedText = `<i>${formattedText}</i>`
|
||||
}
|
||||
|
||||
if (strikethrough) {
|
||||
formattedText = `<s>${formattedText}</s>`
|
||||
}
|
||||
|
||||
parsedRuns.push(formattedText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -437,3 +462,36 @@ export function mapLocalFormat(format) {
|
||||
url: format.url
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('youtubei.js/dist/src/parser/classes/comments/Comment').default} comment
|
||||
* @param {import('youtubei.js/dist/src/parser/classes/comments/CommentThread').default} commentThread
|
||||
*/
|
||||
export function parseLocalComment(comment, commentThread = undefined) {
|
||||
let hasOwnerReplied = false
|
||||
let replyToken = null
|
||||
|
||||
if (commentThread?.has_replies) {
|
||||
hasOwnerReplied = commentThread.comment_replies_data.has_channel_owner_replied
|
||||
replyToken = commentThread
|
||||
}
|
||||
|
||||
return {
|
||||
dataType: 'local',
|
||||
authorLink: comment.author.id,
|
||||
author: comment.author.name,
|
||||
authorThumb: comment.author.best_thumbnail.url,
|
||||
isPinned: comment.is_pinned,
|
||||
isOwner: comment.author_is_channel_owner,
|
||||
isMember: comment.is_member,
|
||||
text: Autolinker.link(parseLocalTextRuns(comment.content.runs, 16)),
|
||||
time: toLocalePublicationString({ publishText: comment.published.text.replace('(edited)', '').trim() }),
|
||||
likes: comment.vote_count,
|
||||
isHearted: comment.is_hearted,
|
||||
numReplies: comment.reply_count,
|
||||
hasOwnerReplied,
|
||||
replyToken,
|
||||
showReplies: false,
|
||||
replies: []
|
||||
}
|
||||
}
|
||||
|
20
yarn.lock
20
yarn.lock
@ -1058,13 +1058,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.9.tgz#7df35818135be54fd87568f16ec7b998a3d0cce8"
|
||||
integrity sha512-tUmO92PFHbLOplitjHNBVGMJm6S57vp16tBXJVPKSI/6CfjrgLycqKxEpC6f7qsOqUdoXs5nIv4HLUfrOMHzuw==
|
||||
|
||||
"@freetube/yt-comment-scraper@^6.2.0":
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@freetube/yt-comment-scraper/-/yt-comment-scraper-6.2.0.tgz#ed11d65111d03076ff842eb9c3eb25413f8632ab"
|
||||
integrity sha512-69mBsvQ50rUBTUDfR6s1OaiH2sEmmx9T0uV2Qpp0ckMhL0sxxMZtxujraHV2FHDtOm5jG7uygi3Gseb54C9DGw==
|
||||
dependencies:
|
||||
axios "^0.27.2"
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.8":
|
||||
version "0.11.8"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
|
||||
@ -2080,14 +2073,6 @@ aws4@^1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
||||
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
||||
|
||||
axios@^0.27.2:
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
|
||||
integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.9"
|
||||
form-data "^4.0.0"
|
||||
|
||||
axios@^1.1.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35"
|
||||
@ -4395,11 +4380,6 @@ follow-redirects@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4"
|
||||
integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==
|
||||
|
||||
follow-redirects@^1.14.9:
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
|
||||
integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
|
||||
|
||||
follow-redirects@^1.15.0:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||
|
Loading…
Reference in New Issue
Block a user