diff --git a/package.json b/package.json index f90edb335..d69ed3d0c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/renderer/components/watch-video-comments/watch-video-comments.js b/src/renderer/components/watch-video-comments/watch-video-comments.js index bad2f65b5..9ea5a3e00 100644 --- a/src/renderer/components/watch-video-comments/watch-video-comments.js +++ b/src/renderer/components/watch-video-comments/watch-video-comments.js @@ -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, `${emoji.text.substring(2, emoji.text.length - 1)}`) - }) - } - - 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 } diff --git a/src/renderer/components/watch-video-comments/watch-video-comments.vue b/src/renderer/components/watch-video-comments/watch-video-comments.vue index 3e46ec668..65962828e 100644 --- a/src/renderer/components/watch-video-comments/watch-video-comments.vue +++ b/src/renderer/components/watch-video-comments/watch-video-comments.vue @@ -104,11 +104,14 @@ @timestamp-event="onTimestamp" />

- - {{ comment.likes }} + > + + {{ comment.likes }} +

- - - {{ reply.author }} - - + {{ reply.author }} +

- - {{ reply.likes }} + > + + {{ reply.likes }} +

`) } 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(`${text}`) + case 'WEB_PAGE_TYPE_CHANNEL': { + const trimmedText = text.trim() + if (trimmedText.startsWith('@')) { + parsedRuns.push(`${trimmedText}`) } 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 = `${formattedText}` + } + + if (italics) { + formattedText = `${formattedText}` + } + + if (strikethrough) { + formattedText = `${formattedText}` + } + + 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: [] + } +} diff --git a/yarn.lock b/yarn.lock index 7e35637d0..ca0c578f2 100644 --- a/yarn.lock +++ b/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"