Skip to content

Commit

Permalink
refactor!: remove unused functionality and simplify paginated item re…
Browse files Browse the repository at this point in the history
…trieval
  • Loading branch information
brandonbothell committed Jun 13, 2023
1 parent 14ae228 commit 28f40ec
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 90 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"new-parens": "error",
"no-caller": "error",
"no-cond-assign": "error",
"no-constant-condition": "error",
"no-constant-condition": [ "error", { "checkLoops": false } ],
"no-control-regex": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "error",
Expand Down
2 changes: 2 additions & 0 deletions src/entities/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,11 @@ export class Channel {
}

/**
* @deprecated See https://support.google.com/youtube/thread/130882091?hl=en&msgid=131295194
* Fetches the channel's discussion tab comments and assigns them to Channel.comments.
* @param maxResults The maximum amount of comments to fetch
*/
/* istanbul ignore next */
public async fetchComments (maxResults: number = 10, parts?: CommentThreadParts) {
this.comments = await this.youtube.getChannelComments(this.id, maxResults, parts)
return this.comments
Expand Down
48 changes: 33 additions & 15 deletions src/entities/comment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { YouTube } from '..'
import { Channel, Video, YouTube } from '..'
import { CommentParts } from '../types/Parts'

export class YTComment {
Expand Down Expand Up @@ -109,12 +109,19 @@ export class YTComment {
public dateEdited: Date

/**
* Either the ID of the video that it is commenting on, the ID of the
* comment it is replying to, or the ID of the channel it is commenting
* on.
* Undefined whenever the comment is fetched directly using its ID.
* The ID of the channel that uploaded the video this comment is on, if any.
*/
public parentId: string
public channelId?: string

/**
* The ID of the video that this comment is on, if any.
*/
public videoId?: string

/**
* If this comment is a reply, then this is the ID of the comment it is replying to.
*/
public parentCommentId?: string

/**
* Replies directed to the comment. If the comment was fetched from a video,
Expand All @@ -123,17 +130,18 @@ export class YTComment {
*/
public replies: YTComment[]

constructor (youtube: YouTube, data: any, full = true, type: 'video' | 'channel') {
constructor (youtube: YouTube, data: any, full = true, replies?: any[]) {
this.youtube = youtube
this.data = data
if (replies) this.data.replies = replies

this._init(data, type)
this._init(data, replies)
}

/**
* @ignore
*/
private _init (data: any, type: 'video' | 'channel') {
private _init (data: any, replies?: any[]) {
if (data.kind !== 'youtube#comment') {
throw new Error(`Invalid comment type: ${data.kind}`)
}
Expand All @@ -145,28 +153,38 @@ export class YTComment {
this.author = {
username: comment.snippet.authorDisplayName,
avatar: comment.snippet.authorProfileImageUrl,
channelId: comment.snippet.authorChannelId.value,
channelId: comment.snippet.authorChannelId?.value,
channelUrl: comment.snippet.authorChannelUrl
}

this.text = {
displayed: comment.snippet.textDisplay,
original: comment.snippet.textOriginal
}

this.rateable = comment.snippet.canRate
this.popular = comment.snippet.likeCount >= 100
this.likes = comment.snippet.likeCount
this.datePublished = comment.snippet.publishedAt
this.dateEdited = comment.snippet.updatedAt
this.parentId = comment.snippet.parentId ? comment.snippet.parentId : comment.snippet.videoId ? comment.snippet.videoId : comment.snippet.channelId
this.channelId = comment.snippet.channelId
this.videoId = comment.snippet.videoId
this.parentCommentId = comment.snippet.parentId

if (comment.snippet.videoId) {
this.url = `https://youtube.com/watch?v=${comment.snippet.videoId}&lc=${comment.id}`
}
}

this.id = comment.id
this.replies = []

if (this.parentId) {
this.url = 'https://youtube.com/' + (type === 'channel' ? `channel/${this.parentId}/discussion?lc=${this.id}` : `watch?v=${this.parentId}&lc=${this.id}`)
if (replies) {
for (const replyData of replies) {
this.replies.push(new YTComment(this.youtube, replyData))
}
}

this.replies = []
this.id = comment.id
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/entities/playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class Playlist {
/**
* Removes a [[Video]] from the playlist.
* Must be using an access token with correct scopes.
* @param playlistItemId The playlist item ID (not the same as video id. See [[Playlist.removeVideo()]]).
* @param playlistItemId The playlist item ID (not the same as video id. See [[Playlist.removeVideo]]).
*/
/* istanbul ignore next */
public async removeItem (playlistItemId: string) {
Expand Down
2 changes: 1 addition & 1 deletion src/entities/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class Video {
public data: any

/**
* Whether or not this is a full video object (would it be the same if we ran [[Video.fetch()]] under the same conditions as last time?).
* Whether or not this is a full video object (would it be the same if we ran [[Video.fetch]] under the same conditions as last time?).
*/
public full: boolean

Expand Down
11 changes: 4 additions & 7 deletions src/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,14 @@ export class OAuth {
}

const result = await this.youtube._request.post('commentThreads', { part: 'snippet' }, JSON.stringify(data), null, this.youtube.accessToken)
const type = result.snippet.channelId ? 'channel' : 'video'
return new YTComment(this.youtube, result.snippet.topLevelComment, true, type)
return new YTComment(this.youtube, result.snippet.topLevelComment, true)
}

/**
* Replies to a [[YTComment]].
* Last tested 05/18/2020 11:48. PASSING
* @param commentId The ID of the comment to reply to.
* @param text The text to reply with.
* @param commentType What this comment is on - defaults to video.
* Required for [[YTComment.url]] to be correct.
*/
public async replyToComment (commentId: string, text: string) {
Expand All @@ -123,7 +121,7 @@ export class OAuth {
data.snippet = { parentId: commentId, textOriginal: text }

const response = await this.youtube._request.post('comments', { part: 'id,snippet' }, JSON.stringify(data), null, this.youtube.accessToken)
return new YTComment(this.youtube, response, true, response.snippet.channelId ? 'channel' : 'video')
return new YTComment(this.youtube, response, true)
}

/**
Expand All @@ -142,12 +140,11 @@ export class OAuth {
console.log(data)

const result = await this.youtube._request.put('comments', { part: 'snippet' }, JSON.stringify(data), null, this.youtube.accessToken)
const type = result.snippet.channelId ? 'channel' : 'video'
const comment = new YTComment(this.youtube, result, true, type)
const comment = new YTComment(this.youtube, result, true)

if (result.replies) {
result.replies.comments.forEach(reply => {
const created = new YTComment(this.youtube, reply, true, type)
const created = new YTComment(this.youtube, reply, true)
comment.replies.push(created)
})
}
Expand Down
91 changes: 31 additions & 60 deletions src/services/generic-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class GenericService {
/* istanbul ignore next */
public static async getItem (youtube: YouTube, type: ItemTypes, mine: boolean, id?: string, parts?: string[]): Promise<ItemReturns> {
if (!([ Video, Channel, Playlist, YTComment, Subscription, VideoCategory, VideoAbuseReportReason, ChannelSection, Caption ].includes(type))) {
return Promise.reject('Type must be a video, channel, playlist, comment, subscription, video category, or channel section.')
return Promise.reject(`${type.name}s cannot be directly fetched. The item may be paginated `)
}

if (!mine && id == null) {
Expand Down Expand Up @@ -48,13 +48,7 @@ export class GenericService {
return Promise.reject('Item not found')
}

let endResult: ItemReturns

if (type === YTComment) {
endResult = new type(youtube, result.items[0], true, result.items[0].snippet.channelId ? 'channel' : 'video')
} else {
endResult = new (type as typeof Video)(youtube, result.items[0], true)
}
let endResult: ItemReturns = new (type)(youtube, result.items[0], true)

endResult.full = true

Expand All @@ -81,13 +75,11 @@ export class GenericService {
}

if (youtube._shouldCache) {
const cached = Cache.get(`getpage://${type}/${id ? id : 'mine'}/${maxResults}`)
const cached = Cache.get(`getpage://${type}/${mine}/${id}/${subId}/${parts?.join(',')}/${maxResults}`)
if (cached) return cached
}

let items = []

const full = maxResults <= 0
const fetchAll = maxResults <= 0
const options: {
part: string
maxResults?: number
Expand All @@ -105,7 +97,7 @@ export class GenericService {
}

let endpoint: string
let max: number
let maxForEndpoint: number
let clazz: typeof Video | typeof YTComment | typeof Playlist | typeof Subscription | typeof VideoCategory | typeof VideoAbuseReportReason | typeof Language |
typeof Region | typeof ChannelSection | typeof Caption
let commentType: 'video' | 'channel'
Expand All @@ -114,7 +106,7 @@ export class GenericService {
switch (type) {
case PaginatedItemType.PlaylistItems:
endpoint = 'playlistItems'
max = 50
maxForEndpoint = 50
clazz = Video
options.playlistId = id
if (!options.part.includes('snippet')) options.part += ',snippet'
Expand All @@ -129,7 +121,7 @@ export class GenericService {
if (!commentType) commentType = 'channel'

endpoint = 'commentThreads'
max = 100
maxForEndpoint = 100
clazz = YTComment
options[`${commentType}Id`] = id
if (!options.part.includes('snippet')) options.part += ',snippet'
Expand All @@ -138,7 +130,7 @@ export class GenericService {

case PaginatedItemType.CommentReplies:
endpoint = 'comments'
max = 100
maxForEndpoint = 100
clazz = YTComment
options.parentId = id
if (!options.part.includes('snippet')) options.part += ',snippet'
Expand All @@ -153,7 +145,7 @@ export class GenericService {
if (!endpoint) endpoint = 'subscriptions'
if (!clazz) clazz = Subscription

max = 50
maxForEndpoint = 50
// falls through

case PaginatedItemType.ChannelSections:
Expand Down Expand Up @@ -195,64 +187,43 @@ export class GenericService {
return Promise.reject('Unknown item type: ' + type)
}

if (max && maxResults > max) {
return Promise.reject(`Max results must be ${max} or below for ${endpoint}`)
}
if (maxForEndpoint !== undefined) {
if (maxResults > maxForEndpoint) {
return Promise.reject(`Max results must be ${maxForEndpoint} or below for ${endpoint}`)
}

if (max) {
options.maxResults = full ? max : maxResults
options.maxResults = fetchAll ? maxForEndpoint : maxResults
}

let results
let pages = -1
let shouldReturn = !full

for (let i = 1; i < (pages > 0 ? pages : 3); i++) {
results = await youtube._request.api(endpoint, options, youtube.token, youtube.accessToken)

if (results.items.length === 0) {
return []
}

if (pages < 1) {
pages = results.pageInfo ? results.pageInfo.totalResults / results.pageInfo.resultsPerPage : 0
const items: InstanceType<typeof clazz>[] = []
let apiResponse: any
let shouldBreak = fetchAll ? false : true

if (pages <= 1) {
shouldReturn = true
}
while (true) {
apiResponse = await youtube._request.api(endpoint, options, youtube.token, youtube.accessToken)

pages = Math.floor(pages)
if (!apiResponse.items?.length) {
break
}

for (let i = 0; i < results.items.length; i++) {
const item = results.items[i]
let comment: YTComment

if (item.snippet && item.snippet.topLevelComment) {
comment = new YTComment(youtube, item.snippet.topLevelComment, false, commentType)
items.push(comment)
for (const data of apiResponse.items) {
if (data.kind === 'youtube#commentThread') {
items.push(new YTComment(youtube, data.snippet.topLevelComment, true, data.replies))
} else {
items.push(new clazz(youtube, item, false, commentType))
}

if (item.replies) {
item.replies.comments.forEach(reply => {
const created = new YTComment(youtube, reply, false, commentType)
comment.replies.push(created)
})
items.push(new clazz(youtube, data, false))
}
}

if (results.nextPageToken && !shouldReturn) {
options.pageToken = results.nextPageToken
} else {
return items
if (shouldBreak || !apiResponse.nextPageToken) {
break
}

options.pageToken = apiResponse.nextPageToken
}

if (youtube._shouldCache) youtube._cache(`getpage://${endpoint}/${id ? id : 'mine'}/${maxResults}`, items)
if (youtube._shouldCache) youtube._cache(`getpage://${type}/${id ? id : 'mine'}/${subId}/${parts?.join(',')}/${maxResults}`, items)

return items
return items as PaginatedItemsReturns
}

/* istanbul ignore next */
Expand Down
8 changes: 5 additions & 3 deletions test/comments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('Comments', () => {
})

// Youtube has REMOVED the discussion tab from channels
it('should work with fetching from a channel object', async () => {
it('should not work with fetching from a channel object', async () => {
const channel = await youtube.getChannel('UC6mi9rp7vRYninucP61qOjg', [ 'id' ])

expect(await channel.fetchComments(1, [ 'id' ]).catch(error => {
Expand Down Expand Up @@ -66,11 +66,13 @@ describe('Comments', () => {
expect(await youtube.getComment('Ugyv3oMTx4CLRXS-9BZ4AaABAg')).to.be.an.instanceOf(YTComment)
})

it('should have a parent ID of its video', async () => {
it('should have the ID of its video', async () => {
const video = await youtube.getVideo('Lq1D8PFnjWY', [ 'id' ])
const comments = await video.fetchComments(1, [ 'snippet' ])

expect(comments[0].parentId).to.equal('Lq1D8PFnjWY')
expect(comments[0].videoId).to.equal('Lq1D8PFnjWY')
// expect(comments[0].channelId).to.equal('UC6mi9rp7vRYninucP61qOjg')
expect(comments[0].channelId).to.equal(undefined) // broken in the API!
})

it('should have a correct URL', async () => {
Expand Down
4 changes: 2 additions & 2 deletions test/replies.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ describe('Replies', () => {
expect((await youtube.getCommentReplies('Ugyv3oMTx4CLRXS-9BZ4AaABAg', 1)).length).to.be.lessThan(2)
})

it('should have a parent ID of the comment it replied to', async () => {
it('should have the ID of the comment it replied to', async () => {
const comment = await youtube.getComment('Ugyv3oMTx4CLRXS-9BZ4AaABAg')
const replies = await comment.fetchReplies(1)

expect(replies[0].parentId).to.equal('Ugyv3oMTx4CLRXS-9BZ4AaABAg')
expect(replies[0].parentCommentId).to.equal('Ugyv3oMTx4CLRXS-9BZ4AaABAg')
})
})

0 comments on commit 28f40ec

Please sign in to comment.