From 6c94873dfa01623d734375d450eb7b2ef5118bbb Mon Sep 17 00:00:00 2001 From: ricardo Date: Wed, 1 Jul 2020 22:54:51 -0400 Subject: [PATCH 1/6] :sparkles: Youtube-Node --- .../YouTubeOAuth2Api.credentials.ts | 28 + .../Google/YouTube/ChannelDescription.ts | 486 ++++++++++++++++++ .../nodes/Google/YouTube/GenericFunctions.ts | 66 +++ .../nodes/Google/YouTube/YouTube.node.ts | 309 +++++++++++ .../nodes/Google/YouTube/youTube.png | Bin 0 -> 1038 bytes packages/nodes-base/package.json | 4 +- 6 files changed, 892 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/credentials/YouTubeOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts create mode 100644 packages/nodes-base/nodes/Google/YouTube/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts create mode 100644 packages/nodes-base/nodes/Google/YouTube/youTube.png diff --git a/packages/nodes-base/credentials/YouTubeOAuth2Api.credentials.ts b/packages/nodes-base/credentials/YouTubeOAuth2Api.credentials.ts new file mode 100644 index 0000000000..46171f5114 --- /dev/null +++ b/packages/nodes-base/credentials/YouTubeOAuth2Api.credentials.ts @@ -0,0 +1,28 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +//https://developers.google.com/youtube/v3/guides/auth/client-side-web-apps#identify-access-scopes +const scopes = [ + 'https://www.googleapis.com/auth/youtube', + 'https://www.googleapis.com/auth/youtubepartner', + 'https://www.googleapis.com/auth/youtube.force-ssl', + 'https://www.googleapis.com/auth/youtube.upload', +]; + +export class YouTubeOAuth2Api implements ICredentialType { + name = 'youTubeOAuth2Api'; + extends = [ + 'googleOAuth2Api', + ]; + displayName = 'Google OAuth2 API'; + properties = [ + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts new file mode 100644 index 0000000000..80cb5b8127 --- /dev/null +++ b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts @@ -0,0 +1,486 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const channelOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'channel', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all channels', + }, + { + name: 'Update', + value: 'update', + description: 'Update a channel', + }, + { + name: 'Upload Banner', + value: 'uploadBanner', + description: 'Upload a channel banner', + } + ], + default: 'getAll', + description: 'The operation to perform.' + } +] as INodeProperties[]; + +export const channelFields = [ + /* -------------------------------------------------------------------------- */ + /* channel:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Part', + name: 'part', + type: 'multiOptions', + options: [ + { + name: 'Audit Details', + value: 'auditDetails', + }, + { + name: 'Branding Settings', + value: 'brandingSettings', + }, + { + name: 'Content Details', + value: 'contentDetails', + }, + { + name: 'Content Owner Details', + value: 'contentOwnerDetails', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Localizations', + value: 'localizations', + }, + { + name: 'Snippet', + value: 'snippet', + }, + { + name: 'Statistics', + value: 'statistics', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Topic Details', + value: 'topicDetails', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'channel', + ], + }, + }, + default: '' + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'channel', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'channel', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'channel', + ], + }, + }, + options: [ + { + displayName: 'Category ID', + name: 'categoryId', + type: 'string', + default: '', + description: 'The categoryId parameter specifies a YouTube guide category, thereby requesting YouTube channels associated with that category.', + }, + { + displayName: 'For Username', + name: 'forUsername', + type: 'string', + default: '', + description: `The forUsername parameter specifies a YouTube username, thereby requesting the channel associated with that username.`, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: `The id parameter specifies a comma-separated list of the YouTube channel ID(s) for the resource(s) that are being retrieved. In a channel resource, the id property specifies the channel's YouTube channel ID.`, + }, + { + displayName: 'Managed By Me', + name: 'managedByMe', + type: 'boolean', + default: false, + description: `Set this parameter's value to true to instruct the API to only return channels managed by the content owner that the onBehalfOfContentOwner parameter specifies`, + }, + { + displayName: 'Mine', + name: 'mine', + type: 'boolean', + default: false, + description: `This parameter can only be used in a properly authorized request. Set this parameter's value to true to instruct the API to only return channels owned by the authenticated user.`, + }, + { + displayName: 'Language Code', + name: 'h1', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages' + }, + default: '', + description: `The hl parameter instructs the API to retrieve localized resource metadata for a specific application language that the YouTube website supports.`, + }, + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* channel:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'channel', + ], + }, + }, + options: [ + { + displayName: 'Branding Settings', + name: 'brandingSettingsUi', + type: 'fixedCollection', + default: {}, + description: 'Encapsulates information about the branding of the channel.', + placeholder: 'Add Branding Settings', + typeOptions: { + multipleValues: false, + }, + options: [ + { + name: 'channelSettingsValues', + displayName: 'Channel Settings', + values: [ + { + displayName: 'Channel', + name: 'channel', + type: 'collection', + default: '', + placeholder: 'Add Channel Settings', + typeOptions: { + multipleValues: false, + }, + options: [ + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + description: 'The country with which the channel is associated. Update this property to set the value of the snippet.country property.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: `The channel description, which appears in the channel information box on your channel page. The property's value has a maximum length of 1000 characters.`, + }, + { + displayName: 'Default Language', + name: 'defaultLanguage', + type: 'string', + default: '', + description: 'The content tab that users should display by default when viewers arrive at your channel page.', + }, + { + displayName: 'Default Tab', + name: 'defaultTab', + type: 'string', + default: 'The content tab that users should display by default when viewers arrive at your channel page.', + }, + { + displayName: 'Featured Channels Title', + name: 'featuredChannelsTitle', + type: 'string', + default: '', + description: 'The title that displays above the featured channels module. The title has a maximum length of 30 characters.', + }, + { + displayName: 'Featured Channels Urls', + name: 'featuredChannelsUrls', + type: 'string', + typeOptions: { + multipleValues: true, + }, + description: 'A list of up to 100 channels that you would like to link to from the featured channels module. The property value is a list of YouTube channel ID values, each of which uniquely identifies a channel.', + default: [], + }, + { + displayName: 'Keywords', + name: 'keywords', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + placeholder: 'tech,news', + description: 'Keywords associated with your channel. The value is a space-separated list of strings.', + default: '', + }, + { + displayName: 'Moderate Comments', + name: 'moderateComments', + type: 'boolean', + description: 'This setting determines whether user-submitted comments left on the channel page need to be approved by the channel owner to be publicly visible.', + default: false, + }, + { + displayName: 'Profile Color', + name: 'profileColor', + type: 'string', + default: '', + description: `A prominent color that complements the channel's content.`, + }, + { + displayName: 'Show Related Channels', + name: 'showRelatedChannels', + type: 'boolean', + description: 'This setting indicates whether YouTube should show an algorithmically generated list of related channels on your channel page.', + default: false, + }, + { + displayName: 'Show Browse View', + name: 'showBrowseView', + type: 'boolean', + description: 'This setting indicates whether the channel page should display content in a browse or feed view.', + default: false, + }, + { + displayName: 'Tracking Analytics AccountId', + name: 'trackingAnalyticsAccountId', + type: 'string', + description: 'The ID for a Google Analytics account that you want to use to track and measure traffic to your channel.', + default: '', + }, + { + displayName: 'Unsubscribed Trailer', + name: 'unsubscribedTrailer', + type: 'string', + description: `The video that should play in the featured video module in the channel page's browse view for unsubscribed viewers.`, + default: '', + }, + ], + }, + ], + description: 'The channel object encapsulates branding properties of the channel page', + }, + { + name: 'imageSettingsValues', + displayName: 'Image Settings', + values: [ + { + displayName: 'Image', + name: 'image', + type: 'collection', + default: '', + placeholder: 'Add Channel Settings', + description: `The image object encapsulates information about images that display on the channel's channel page or video watch pages.`, + typeOptions: { + multipleValues: false, + }, + options: [ + { + displayName: 'Banner External Url', + name: 'bannerExternalUrl', + type: 'string', + default: '', + }, + { + displayName: 'Tracking Image Url', + name: 'trackingImageUrl', + type: 'string', + default: '', + }, + { + displayName: 'watch Icon Image Url', + name: 'watchIconImageUrl', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + name: 'statusValue', + displayName: 'Status', + values: [ + { + displayName: 'Status', + name: 'status', + type: 'collection', + default: '', + placeholder: 'Add Status', + typeOptions: { + multipleValues: false, + }, + options: [ + { + displayName: 'Self Declared Made For Kids', + name: 'selfDeclaredMadeForKids', + type: 'boolean', + default: false, + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* channel:uploadBanner */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'uploadBanner', + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'uploadBanner', + ], + resource: [ + 'channel', + ], + }, + }, + default: 'data', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/YouTube/GenericFunctions.ts b/packages/nodes-base/nodes/Google/YouTube/GenericFunctions.ts new file mode 100644 index 0000000000..174f5755d5 --- /dev/null +++ b/packages/nodes-base/nodes/Google/YouTube/GenericFunctions.ts @@ -0,0 +1,66 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://www.googleapis.com${resource}`, + json: true + }; + try { + options = Object.assign({}, options, option); + + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'youTubeOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + let errors = error.response.body.error.errors; + + errors = errors.map((e: IDataObject) => e.message); + // Try to return the error prettier + throw new Error( + `YouTube error response [${error.statusCode}]: ${errors.join('|')}` + ); + } + throw error; + } +} + +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.maxResults = 100; + + do { + responseData = await googleApiRequest.call(this, method, endpoint, body, query); + query.pageToken = responseData['nextPageToken']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['nextPageToken'] !== undefined && + responseData['nextPageToken'] !== '' + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts new file mode 100644 index 0000000000..4621c06d5b --- /dev/null +++ b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts @@ -0,0 +1,309 @@ +import { + IExecuteFunctions, BINARY_ENCODING, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + googleApiRequest, + googleApiRequestAllItems, +} from './GenericFunctions'; + +import { + channelOperations, + channelFields, +} from './ChannelDescription'; + +export class YouTube implements INodeType { + description: INodeTypeDescription = { + displayName: 'Youtube', + name: 'youTube', + icon: 'file:youTube.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume YouTube API.', + defaults: { + name: 'YouTube', + color: '#FF0000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'youTubeOAuth2Api', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Channel', + value: 'channel', + }, + ], + default: 'channel', + description: 'The resource to operate on.' + }, + ...channelOperations, + ...channelFields, + ], + }; + + methods = { + loadOptions: { + // Get all the languages to display them to user so that he can + // select them easily + async getLanguages( + this: ILoadOptionsFunctions + ): Promise { + const returnData: INodePropertyOptions[] = []; + const languages = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + '/youtube/v3/i18nLanguages', + ); + for (const language of languages) { + const languageName = language.id.toUpperCase(); + const languageId = language.id; + returnData.push({ + name: languageName, + value: languageId + }); + } + return returnData; + }, + } + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'channel') { + //https://developers.google.com/youtube/v3/docs/channels/list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const part = this.getNodeParameter('part', i) as string[]; + const options = this.getNodeParameter('options', i) as IDataObject; + + qs.part = part.join(','); + + if (options.categoryId) { + qs.categoryId = options.categoryId as string; + } + if (options.forUsername) { + qs.forUsername = options.forUsername as string; + } + if (options.id) { + qs.id = options.id as string; + } + if (options.managedByMe) { + qs.managedByMe = options.managedByMe as boolean; + } + if (options.mine) { + qs.mine = options.mine as boolean; + } + if (options.h1) { + qs.h1 = options.h1 as string; + } + if (options.onBehalfOfContentOwner) { + qs.onBehalfOfContentOwner = options.onBehalfOfContentOwner as string; + } + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + `/youtube/v3/channels`, + {}, + qs + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/youtube/v3/channels`, + {}, + qs + ); + responseData = responseData.items; + } + } + //https://developers.google.com/youtube/v3/docs/channels/update + if (operation === 'update') { + const channelId = this.getNodeParameter('channelId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = { + id: channelId, + brandingSettings: { + channel: {}, + image: {}, + }, + }; + + qs.part = 'brandingSettings'; + + if (updateFields.onBehalfOfContentOwner) { + qs.onBehalfOfContentOwner = updateFields.onBehalfOfContentOwner as string; + } + + if (updateFields.brandingSettingsUi) { + const channelSettingsValues = (updateFields.brandingSettingsUi as IDataObject).channelSettingsValues as IDataObject | undefined; + const channelSettings: IDataObject = {}; + if (channelSettingsValues?.channel) { + const channelSettingsOptions = channelSettingsValues.channel as IDataObject; + if (channelSettingsOptions.country) { + channelSettings.country = channelSettingsOptions.country; + } + if (channelSettingsOptions.description) { + channelSettings.description = channelSettingsOptions.description; + } + if (channelSettingsOptions.defaultLanguage) { + channelSettings.defaultLanguage = channelSettingsOptions.defaultLanguage; + } + if (channelSettingsOptions.defaultTab) { + channelSettings.defaultTab = channelSettingsOptions.defaultTab; + } + if (channelSettingsOptions.featuredChannelsTitle) { + channelSettings.featuredChannelsTitle = channelSettingsOptions.featuredChannelsTitle; + } + if (channelSettingsOptions.featuredChannelsUrls) { + channelSettings.featuredChannelsUrls = channelSettingsOptions.featuredChannelsUrls; + } + if (channelSettingsOptions.keywords) { + channelSettings.keywords = channelSettingsOptions.keywords; + } + if (channelSettingsOptions.moderateComments) { + channelSettings.moderateComments = channelSettingsOptions.moderateComments as boolean; + } + if (channelSettingsOptions.profileColor) { + channelSettings.profileColor = channelSettingsOptions.profileColor as string; + } + if (channelSettingsOptions.profileColor) { + channelSettings.profileColor = channelSettingsOptions.profileColor as string; + } + if (channelSettingsOptions.showRelatedChannels) { + channelSettings.showRelatedChannels = channelSettingsOptions.showRelatedChannels as boolean; + } + if (channelSettingsOptions.showBrowseView) { + channelSettings.showBrowseView = channelSettingsOptions.showBrowseView as boolean; + } + if (channelSettingsOptions.trackingAnalyticsAccountId) { + channelSettings.trackingAnalyticsAccountId = channelSettingsOptions.trackingAnalyticsAccountId as string; + } + if (channelSettingsOptions.unsubscribedTrailer) { + channelSettings.unsubscribedTrailer = channelSettingsOptions.unsubscribedTrailer as string; + } + } + + const imageSettingsValues = (updateFields.brandingSettingsUi as IDataObject).imageSettingsValues as IDataObject | undefined; + const imageSettings: IDataObject = {}; + if (imageSettingsValues?.image) { + const imageSettingsOptions = imageSettings.image as IDataObject; + if (imageSettingsOptions.bannerExternalUrl) { + imageSettings.bannerExternalUrl = imageSettingsOptions.bannerExternalUrl as string + } + if (imageSettingsOptions.trackingImageUrl) { + imageSettings.trackingImageUrl = imageSettingsOptions.trackingImageUrl as string + } + if (imageSettingsOptions.watchIconImageUrl) { + imageSettings.watchIconImageUrl = imageSettingsOptions.watchIconImageUrl as string + } + } + + //@ts-ignore + body.brandingSettings.channel = channelSettings; + //@ts-ignore + body.brandingSettings.image = imageSettings; + } + + responseData = await googleApiRequest.call( + this, + 'PUT', + '/youtube/v3/channels', + body, + qs + ); + } + //https://developers.google.com/youtube/v3/docs/channelBanners/insert + if (operation === 'uploadBanner') { + const channelId = this.getNodeParameter('channelId', i) as string; + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + + let mimeType; + + // Is binary file to upload + const item = items[i]; + + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + if (item.binary[binaryProperty] === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + if (item.binary[binaryProperty].mimeType) { + mimeType = item.binary[binaryProperty].mimeType; + } + + const body = Buffer.from(item.binary[binaryProperty].data, BINARY_ENCODING); + + const requestOptions = { + headers: { + 'Content-Type': mimeType, + }, + json: false, + }; + + let response = await googleApiRequest.call(this, 'POST', '/upload/youtube/v3/channelBanners/insert', body, qs, undefined, requestOptions); + + const { url } = JSON.parse(response); + + qs.part = 'brandingSettings' + + responseData = await googleApiRequest.call( + this, + 'PUT', + `/youtube/v3/channels`, + { + id: channelId, + brandingSettings: { + image: { + bannerExternalUrl: url, + }, + }, + }, + qs, + ); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Google/YouTube/youTube.png b/packages/nodes-base/nodes/Google/YouTube/youTube.png new file mode 100644 index 0000000000000000000000000000000000000000..efbe8689101029e7eb5a37e5831b918a75ffe376 GIT binary patch literal 1038 zcmeAS@N?(olHy`uVBq!ia0vp^ZXnFT1|$ph9<=}|mSQK*5Dp-y;YjHK@;M7UB8wRq ztdL+6J-h%zux-J2sboV1A;x!ija9{r=RY(M=#4wZ z%Jgje{lYrF3sb*mh>JUH<(hK(+4iEFQ<5&qR3C{wb3tIk1_ql-CKaIr&FlwmNEAHp zJN`xI$4_Amk%sm0jWK*bKC#}o!*cH2oma}meos^d6smqtTM@Ky-|`?uucbccrv2R_ zzIorh*Y9p^-gq(9@bl-r?-p6>$47N;+^xO0?yXte@5b=;YYs6#dOF$T$Q@B8#_3zC z_$Th<{`W(XYkIul$tQ28GR?Rn%vrc2=FFEZ-d2BIjWqeDc2|Bmp*rwb74?I7AH&adft1_-GHp{ zB*R5lLl;Gz_1}GVS;aD4VPUSHo@-h|d{*1kcuYC@Bhy;s)ghHn^^d}MmR8;M-+ixL z=t2G*;ZiYU1BWd;JVG>nWCve*H&v+xN!1ckORkJke`YmSTN(-~9GzrHQAL=SQFQQvJL)!X)}^ r)N#3w`p@JY<8zI0q;vGd|4`h=C3j!v%J&t(yvyL}>gTe~DWM4f&-vQ1 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index c19d8e2349..56c7813657 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -64,6 +64,7 @@ "dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/GoogleSheetsOAuth2Api.credentials.js", "dist/credentials/GoogleTasksOAuth2Api.credentials.js", + "dist/credentials/YouTubeOAuth2Api.credentials.js", "dist/credentials/GumroadApi.credentials.js", "dist/credentials/HarvestApi.credentials.js", "dist/credentials/HelpScoutOAuth2Api.credentials.js", @@ -202,7 +203,8 @@ "dist/nodes/Google/Calendar/GoogleCalendar.node.js", "dist/nodes/Google/Drive/GoogleDrive.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js", - "dist/nodes/Google/Task/GoogleTasks.node.js", + "dist/nodes/Google/Task/GoogleTasks.node.js", + "dist/nodes/Google/YouTube/YouTube.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", "dist/nodes/Harvest/Harvest.node.js", From 98255f709e09a4a4caacf7825f5d387d327030d0 Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 20 Jul 2020 14:33:09 -0400 Subject: [PATCH 2/6] :zap: Small improvement --- packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts index 80cb5b8127..1d02dbae67 100644 --- a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts @@ -41,7 +41,7 @@ export const channelFields = [ /* channel:getAll */ /* -------------------------------------------------------------------------- */ { - displayName: 'Part', + displayName: 'Fields', name: 'part', type: 'multiOptions', options: [ @@ -97,6 +97,7 @@ export const channelFields = [ ], }, }, + description: 'The fields parameter specifies a comma-separated list of one or more channel resource properties that the API response will include.', default: '' }, { From 1e81b7d54ffa8da01561b2b8e1b7a4bed6500965 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 9 Aug 2020 17:39:28 -0400 Subject: [PATCH 3/6] :zap: Improvements --- .../YouTubeOAuth2Api.credentials.ts | 1 + .../Google/YouTube/ChannelDescription.ts | 23 +- .../nodes/Google/YouTube/CountryCodes.ts | 1579 +++++++++++++++++ .../nodes/Google/YouTube/GenericFunctions.ts | 18 +- .../Google/YouTube/PlaylistDescription.ts | 449 +++++ .../nodes/Google/YouTube/VideoDescription.ts | 862 +++++++++ .../nodes/Google/YouTube/YouTube.node.ts | 534 +++++- 7 files changed, 3450 insertions(+), 16 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/YouTube/CountryCodes.ts create mode 100644 packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts create mode 100644 packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts diff --git a/packages/nodes-base/credentials/YouTubeOAuth2Api.credentials.ts b/packages/nodes-base/credentials/YouTubeOAuth2Api.credentials.ts index 46171f5114..5f1e6bfb9c 100644 --- a/packages/nodes-base/credentials/YouTubeOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/YouTubeOAuth2Api.credentials.ts @@ -9,6 +9,7 @@ const scopes = [ 'https://www.googleapis.com/auth/youtubepartner', 'https://www.googleapis.com/auth/youtube.force-ssl', 'https://www.googleapis.com/auth/youtube.upload', + 'https://www.googleapis.com/auth/youtubepartner-channel-audit', ]; export class YouTubeOAuth2Api implements ICredentialType { diff --git a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts index 1d02dbae67..6db5d3a990 100644 --- a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts @@ -142,8 +142,8 @@ export const channelFields = [ description: 'How many results to return.', }, { - displayName: 'Options', - name: 'options', + displayName: 'Filters', + name: 'filters', type: 'collection', placeholder: 'Add Option', default: {}, @@ -193,6 +193,25 @@ export const channelFields = [ default: false, description: `This parameter can only be used in a properly authorized request. Set this parameter's value to true to instruct the API to only return channels owned by the authenticated user.`, }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'channel', + ], + }, + }, + options: [ { displayName: 'Language Code', name: 'h1', diff --git a/packages/nodes-base/nodes/Google/YouTube/CountryCodes.ts b/packages/nodes-base/nodes/Google/YouTube/CountryCodes.ts new file mode 100644 index 0000000000..3c41a76c7c --- /dev/null +++ b/packages/nodes-base/nodes/Google/YouTube/CountryCodes.ts @@ -0,0 +1,1579 @@ +export const countriesCodes = [ + { + 'name': 'Afghanistan', + 'alpha2': 'AF', + 'alpha3': 'AFG', + 'numeric': '004' + }, + { + 'name': 'Åland Islands', + 'alpha2': 'AX', + 'alpha3': 'ALA', + 'numeric': '248', + 'altName': 'Aland Islands' + }, + { + 'name': 'Albania', + 'alpha2': 'AL', + 'alpha3': 'ALB', + 'numeric': '008' + }, + { + 'name': 'Algeria', + 'alpha2': 'DZ', + 'alpha3': 'DZA', + 'numeric': '012' + }, + { + 'name': 'American Samoa', + 'alpha2': 'AS', + 'alpha3': 'ASM', + 'numeric': '016' + }, + { + 'name': 'Andorra', + 'alpha2': 'AD', + 'alpha3': 'AND', + 'numeric': '020' + }, + { + 'name': 'Angola', + 'alpha2': 'AO', + 'alpha3': 'AGO', + 'numeric': '024' + }, + { + 'name': 'Anguilla', + 'alpha2': 'AI', + 'alpha3': 'AIA', + 'numeric': '660' + }, + { + 'name': 'Antarctica', + 'alpha2': 'AQ', + 'alpha3': 'ATA', + 'numeric': '010' + }, + { + 'name': 'Antigua and Barbuda', + 'alpha2': 'AG', + 'alpha3': 'ATG', + 'numeric': '028' + }, + { + 'name': 'Argentina', + 'alpha2': 'AR', + 'alpha3': 'ARG', + 'numeric': '032' + }, + { + 'name': 'Armenia', + 'alpha2': 'AM', + 'alpha3': 'ARM', + 'numeric': '051' + }, + { + 'name': 'Aruba', + 'alpha2': 'AW', + 'alpha3': 'ABW', + 'numeric': '533' + }, + { + 'name': 'Australia', + 'alpha2': 'AU', + 'alpha3': 'AUS', + 'numeric': '036' + }, + { + 'name': 'Austria', + 'alpha2': 'AT', + 'alpha3': 'AUT', + 'numeric': '040' + }, + { + 'name': 'Azerbaijan', + 'alpha2': 'AZ', + 'alpha3': 'AZE', + 'numeric': '031' + }, + { + 'name': 'Bahamas (the)', + 'alpha2': 'BS', + 'alpha3': 'BHS', + 'numeric': '044', + 'altName': 'Bahamas' + }, + { + 'name': 'Bahrain', + 'alpha2': 'BH', + 'alpha3': 'BHR', + 'numeric': '048' + }, + { + 'name': 'Bangladesh', + 'alpha2': 'BD', + 'alpha3': 'BGD', + 'numeric': '050' + }, + { + 'name': 'Barbados', + 'alpha2': 'BB', + 'alpha3': 'BRB', + 'numeric': '052' + }, + { + 'name': 'Belarus', + 'alpha2': 'BY', + 'alpha3': 'BLR', + 'numeric': '112' + }, + { + 'name': 'Belgium', + 'alpha2': 'BE', + 'alpha3': 'BEL', + 'numeric': '056' + }, + { + 'name': 'Belize', + 'alpha2': 'BZ', + 'alpha3': 'BLZ', + 'numeric': '084' + }, + { + 'name': 'Benin', + 'alpha2': 'BJ', + 'alpha3': 'BEN', + 'numeric': '204' + }, + { + 'name': 'Bermuda', + 'alpha2': 'BM', + 'alpha3': 'BMU', + 'numeric': '060' + }, + { + 'name': 'Bhutan', + 'alpha2': 'BT', + 'alpha3': 'BTN', + 'numeric': '064' + }, + { + 'name': 'Bolivia (Plurinational State of)', + 'alpha2': 'BO', + 'alpha3': 'BOL', + 'numeric': '068', + 'altName': 'Bolivia' + }, + { + 'name': 'Bonaire, Sint Eustatius and Saba', + 'alpha2': 'BQ', + 'alpha3': 'BES', + 'numeric': '535' + }, + { + 'name': 'Bosnia and Herzegovina', + 'alpha2': 'BA', + 'alpha3': 'BIH', + 'numeric': '070' + }, + { + 'name': 'Botswana', + 'alpha2': 'BW', + 'alpha3': 'BWA', + 'numeric': '072' + }, + { + 'name': 'Bouvet Island', + 'alpha2': 'BV', + 'alpha3': 'BVT', + 'numeric': '074' + }, + { + 'name': 'Brazil', + 'alpha2': 'BR', + 'alpha3': 'BRA', + 'numeric': '076' + }, + { + 'name': 'British Indian Ocean Territory (the)', + 'alpha2': 'IO', + 'alpha3': 'IOT', + 'numeric': '086', + 'altName': 'British Indian Ocean Territory' + }, + { + 'name': 'Brunei Darussalam', + 'alpha2': 'BN', + 'alpha3': 'BRN', + 'numeric': '096', + 'shortName': 'Brunei' + }, + { + 'name': 'Bulgaria', + 'alpha2': 'BG', + 'alpha3': 'BGR', + 'numeric': '100' + }, + { + 'name': 'Burkina Faso', + 'alpha2': 'BF', + 'alpha3': 'BFA', + 'numeric': '854' + }, + { + 'name': 'Burundi', + 'alpha2': 'BI', + 'alpha3': 'BDI', + 'numeric': '108' + }, + { + 'name': 'Cabo Verde', + 'alpha2': 'CV', + 'alpha3': 'CPV', + 'numeric': '132', + 'altName': 'Cape Verde' + }, + { + 'name': 'Cambodia', + 'alpha2': 'KH', + 'alpha3': 'KHM', + 'numeric': '116' + }, + { + 'name': 'Cameroon', + 'alpha2': 'CM', + 'alpha3': 'CMR', + 'numeric': '120' + }, + { + 'name': 'Canada', + 'alpha2': 'CA', + 'alpha3': 'CAN', + 'numeric': '124' + }, + { + 'name': 'Cayman Islands (the)', + 'alpha2': 'KY', + 'alpha3': 'CYM', + 'numeric': '136', + 'altName': 'Cayman Islands' + }, + { + 'name': 'Central African Republic (the)', + 'alpha2': 'CF', + 'alpha3': 'CAF', + 'numeric': '140', + 'altName': 'Central African Republic' + }, + { + 'name': 'Chad', + 'alpha2': 'TD', + 'alpha3': 'TCD', + 'numeric': '148' + }, + { + 'name': 'Chile', + 'alpha2': 'CL', + 'alpha3': 'CHL', + 'numeric': '152' + }, + { + 'name': 'China', + 'alpha2': 'CN', + 'alpha3': 'CHN', + 'numeric': '156' + }, + { + 'name': 'Christmas Island', + 'alpha2': 'CX', + 'alpha3': 'CXR', + 'numeric': '162' + }, + { + 'name': 'Cocos (Keeling) Islands (the)', + 'alpha2': 'CC', + 'alpha3': 'CCK', + 'numeric': '166', + 'altName': 'Cocos (Keeling) Islands', + 'shortName': 'Cocos Islands' + }, + { + 'name': 'Colombia', + 'alpha2': 'CO', + 'alpha3': 'COL', + 'numeric': '170' + }, + { + 'name': 'Comoros (the)', + 'alpha2': 'KM', + 'alpha3': 'COM', + 'numeric': '174', + 'altName': 'Comoros' + }, + { + 'name': 'Congo (the Democratic Republic of the)', + 'alpha2': 'CD', + 'alpha3': 'COD', + 'numeric': '180', + 'altName': 'Congo, (Kinshasa)', + 'shortName': 'Democratic Republic of the Congo' + }, + { + 'name': 'Congo (the)', + 'alpha2': 'CG', + 'alpha3': 'COG', + 'numeric': '178', + 'altName': 'Congo (Brazzaville)', + 'shortName': 'Republic of the Congo' + }, + { + 'name': 'Cook Islands (the)', + 'alpha2': 'CK', + 'alpha3': 'COK', + 'numeric': '184', + 'altName': 'Cook Islands' + }, + { + 'name': 'Costa Rica', + 'alpha2': 'CR', + 'alpha3': 'CRI', + 'numeric': '188' + }, + { + 'name': 'Côte d\'Ivoire', + 'alpha2': 'CI', + 'alpha3': 'CIV', + 'numeric': '384', + 'shortName': 'Ivory Coast' + }, + { + 'name': 'Croatia', + 'alpha2': 'HR', + 'alpha3': 'HRV', + 'numeric': '191' + }, + { + 'name': 'Cuba', + 'alpha2': 'CU', + 'alpha3': 'CUB', + 'numeric': '192' + }, + { + 'name': 'Curaçao', + 'alpha2': 'CW', + 'alpha3': 'CUW', + 'numeric': '531', + 'shortName': 'Curacao' + }, + { + 'name': 'Cyprus', + 'alpha2': 'CY', + 'alpha3': 'CYP', + 'numeric': '196' + }, + { + 'name': 'Czechia', + 'alpha2': 'CZ', + 'alpha3': 'CZE', + 'numeric': '203', + 'altName': 'Czech Republic' + }, + { + 'name': 'Denmark', + 'alpha2': 'DK', + 'alpha3': 'DNK', + 'numeric': '208' + }, + { + 'name': 'Djibouti', + 'alpha2': 'DJ', + 'alpha3': 'DJI', + 'numeric': '262' + }, + { + 'name': 'Dominica', + 'alpha2': 'DM', + 'alpha3': 'DMA', + 'numeric': '212' + }, + { + 'name': 'Dominican Republic (the)', + 'alpha2': 'DO', + 'alpha3': 'DOM', + 'numeric': '214', + 'altName': 'Dominican Republic' + }, + { + 'name': 'Ecuador', + 'alpha2': 'EC', + 'alpha3': 'ECU', + 'numeric': '218' + }, + { + 'name': 'Egypt', + 'alpha2': 'EG', + 'alpha3': 'EGY', + 'numeric': '818' + }, + { + 'name': 'El Salvador', + 'alpha2': 'SV', + 'alpha3': 'SLV', + 'numeric': '222' + }, + { + 'name': 'Equatorial Guinea', + 'alpha2': 'GQ', + 'alpha3': 'GNQ', + 'numeric': '226' + }, + { + 'name': 'Eritrea', + 'alpha2': 'ER', + 'alpha3': 'ERI', + 'numeric': '232' + }, + { + 'name': 'Estonia', + 'alpha2': 'EE', + 'alpha3': 'EST', + 'numeric': '233' + }, + { + 'name': 'Ethiopia', + 'alpha2': 'ET', + 'alpha3': 'ETH', + 'numeric': '231' + }, + { + 'name': 'Falkland Islands (the) [Malvinas]', + 'alpha2': 'FK', + 'alpha3': 'FLK', + 'numeric': '238', + 'altName': 'Falkland Islands (Malvinas)', + 'shortName': 'Falkland Islands' + }, + { + 'name': 'Faroe Islands (the)', + 'alpha2': 'FO', + 'alpha3': 'FRO', + 'numeric': '234', + 'altName': 'Faroe Islands' + }, + { + 'name': 'Fiji', + 'alpha2': 'FJ', + 'alpha3': 'FJI', + 'numeric': '242' + }, + { + 'name': 'Finland', + 'alpha2': 'FI', + 'alpha3': 'FIN', + 'numeric': '246' + }, + { + 'name': 'France', + 'alpha2': 'FR', + 'alpha3': 'FRA', + 'numeric': '250' + }, + { + 'name': 'French Guiana', + 'alpha2': 'GF', + 'alpha3': 'GUF', + 'numeric': '254' + }, + { + 'name': 'French Polynesia', + 'alpha2': 'PF', + 'alpha3': 'PYF', + 'numeric': '258' + }, + { + 'name': 'French Southern Territories (the)', + 'alpha2': 'TF', + 'alpha3': 'ATF', + 'numeric': '260', + 'altName': 'French Southern Territories' + }, + { + 'name': 'Gabon', + 'alpha2': 'GA', + 'alpha3': 'GAB', + 'numeric': '266' + }, + { + 'name': 'Gambia (the)', + 'alpha2': 'GM', + 'alpha3': 'GMB', + 'numeric': '270', + 'altName': 'Gambia' + }, + { + 'name': 'Georgia', + 'alpha2': 'GE', + 'alpha3': 'GEO', + 'numeric': '268' + }, + { + 'name': 'Germany', + 'alpha2': 'DE', + 'alpha3': 'DEU', + 'numeric': '276' + }, + { + 'name': 'Ghana', + 'alpha2': 'GH', + 'alpha3': 'GHA', + 'numeric': '288' + }, + { + 'name': 'Gibraltar', + 'alpha2': 'GI', + 'alpha3': 'GIB', + 'numeric': '292' + }, + { + 'name': 'Greece', + 'alpha2': 'GR', + 'alpha3': 'GRC', + 'numeric': '300' + }, + { + 'name': 'Greenland', + 'alpha2': 'GL', + 'alpha3': 'GRL', + 'numeric': '304' + }, + { + 'name': 'Grenada', + 'alpha2': 'GD', + 'alpha3': 'GRD', + 'numeric': '308' + }, + { + 'name': 'Guadeloupe', + 'alpha2': 'GP', + 'alpha3': 'GLP', + 'numeric': '312' + }, + { + 'name': 'Guam', + 'alpha2': 'GU', + 'alpha3': 'GUM', + 'numeric': '316' + }, + { + 'name': 'Guatemala', + 'alpha2': 'GT', + 'alpha3': 'GTM', + 'numeric': '320' + }, + { + 'name': 'Guernsey', + 'alpha2': 'GG', + 'alpha3': 'GGY', + 'numeric': '831' + }, + { + 'name': 'Guinea', + 'alpha2': 'GN', + 'alpha3': 'GIN', + 'numeric': '324' + }, + { + 'name': 'Guinea-Bissau', + 'alpha2': 'GW', + 'alpha3': 'GNB', + 'numeric': '624' + }, + { + 'name': 'Guyana', + 'alpha2': 'GY', + 'alpha3': 'GUY', + 'numeric': '328' + }, + { + 'name': 'Haiti', + 'alpha2': 'HT', + 'alpha3': 'HTI', + 'numeric': '332' + }, + { + 'name': 'Heard Island and McDonald Islands', + 'alpha2': 'HM', + 'alpha3': 'HMD', + 'numeric': '334', + 'altName': 'Heard and Mcdonald Islands' + }, + { + 'name': 'Holy See (the)', + 'alpha2': 'VA', + 'alpha3': 'VAT', + 'numeric': '336', + 'altName': 'Holy See (Vatican City State)', + 'shortName': 'Vatican' + }, + { + 'name': 'Honduras', + 'alpha2': 'HN', + 'alpha3': 'HND', + 'numeric': '340' + }, + { + 'name': 'Hong Kong', + 'alpha2': 'HK', + 'alpha3': 'HKG', + 'numeric': '344', + 'altName': 'Hong Kong, SAR China' + }, + { + 'name': 'Hungary', + 'alpha2': 'HU', + 'alpha3': 'HUN', + 'numeric': '348' + }, + { + 'name': 'Iceland', + 'alpha2': 'IS', + 'alpha3': 'ISL', + 'numeric': '352' + }, + { + 'name': 'India', + 'alpha2': 'IN', + 'alpha3': 'IND', + 'numeric': '356' + }, + { + 'name': 'Indonesia', + 'alpha2': 'ID', + 'alpha3': 'IDN', + 'numeric': '360' + }, + { + 'name': 'Iran (Islamic Republic of)', + 'alpha2': 'IR', + 'alpha3': 'IRN', + 'numeric': '364', + 'altName': 'Iran, Islamic Republic of', + 'shortName': 'Iran' + }, + { + 'name': 'Iraq', + 'alpha2': 'IQ', + 'alpha3': 'IRQ', + 'numeric': '368' + }, + { + 'name': 'Ireland', + 'alpha2': 'IE', + 'alpha3': 'IRL', + 'numeric': '372' + }, + { + 'name': 'Isle of Man', + 'alpha2': 'IM', + 'alpha3': 'IMN', + 'numeric': '833' + }, + { + 'name': 'Israel', + 'alpha2': 'IL', + 'alpha3': 'ISR', + 'numeric': '376' + }, + { + 'name': 'Italy', + 'alpha2': 'IT', + 'alpha3': 'ITA', + 'numeric': '380' + }, + { + 'name': 'Jamaica', + 'alpha2': 'JM', + 'alpha3': 'JAM', + 'numeric': '388' + }, + { + 'name': 'Japan', + 'alpha2': 'JP', + 'alpha3': 'JPN', + 'numeric': '392' + }, + { + 'name': 'Jersey', + 'alpha2': 'JE', + 'alpha3': 'JEY', + 'numeric': '832' + }, + { + 'name': 'Jordan', + 'alpha2': 'JO', + 'alpha3': 'JOR', + 'numeric': '400' + }, + { + 'name': 'Kazakhstan', + 'alpha2': 'KZ', + 'alpha3': 'KAZ', + 'numeric': '398' + }, + { + 'name': 'Kenya', + 'alpha2': 'KE', + 'alpha3': 'KEN', + 'numeric': '404' + }, + { + 'name': 'Kiribati', + 'alpha2': 'KI', + 'alpha3': 'KIR', + 'numeric': '296' + }, + { + 'name': 'Korea (the Democratic People\'s Republic of)', + 'alpha2': 'KP', + 'alpha3': 'PRK', + 'numeric': '408', + 'altName': 'Korea (North)', + 'shortName': 'North Korea' + }, + { + 'name': 'Korea (the Republic of)', + 'alpha2': 'KR', + 'alpha3': 'KOR', + 'numeric': '410', + 'altName': 'Korea (South)', + 'shortName': 'South Korea' + }, + { + 'name': 'Kuwait', + 'alpha2': 'KW', + 'alpha3': 'KWT', + 'numeric': '414' + }, + { + 'name': 'Kyrgyzstan', + 'alpha2': 'KG', + 'alpha3': 'KGZ', + 'numeric': '417' + }, + { + 'name': 'Lao People\'s Democratic Republic (the)', + 'alpha2': 'LA', + 'alpha3': 'LAO', + 'numeric': '418', + 'altName': 'Lao PDR', + 'shortName': 'Laos' + }, + { + 'name': 'Latvia', + 'alpha2': 'LV', + 'alpha3': 'LVA', + 'numeric': '428' + }, + { + 'name': 'Lebanon', + 'alpha2': 'LB', + 'alpha3': 'LBN', + 'numeric': '422' + }, + { + 'name': 'Lesotho', + 'alpha2': 'LS', + 'alpha3': 'LSO', + 'numeric': '426' + }, + { + 'name': 'Liberia', + 'alpha2': 'LR', + 'alpha3': 'LBR', + 'numeric': '430' + }, + { + 'name': 'Libya', + 'alpha2': 'LY', + 'alpha3': 'LBY', + 'numeric': '434' + }, + { + 'name': 'Liechtenstein', + 'alpha2': 'LI', + 'alpha3': 'LIE', + 'numeric': '438' + }, + { + 'name': 'Lithuania', + 'alpha2': 'LT', + 'alpha3': 'LTU', + 'numeric': '440' + }, + { + 'name': 'Luxembourg', + 'alpha2': 'LU', + 'alpha3': 'LUX', + 'numeric': '442' + }, + { + 'name': 'Macao', + 'alpha2': 'MO', + 'alpha3': 'MAC', + 'numeric': '446', + 'altName': 'Macao, SAR China', + 'shortName': 'Macau' + }, + { + 'name': 'Macedonia (the former Yugoslav Republic of)', + 'alpha2': 'MK', + 'alpha3': 'MKD', + 'numeric': '807', + 'altName': 'Macedonia, Republic of', + 'shortName': 'Macedonia' + }, + { + 'name': 'Madagascar', + 'alpha2': 'MG', + 'alpha3': 'MDG', + 'numeric': '450' + }, + { + 'name': 'Malawi', + 'alpha2': 'MW', + 'alpha3': 'MWI', + 'numeric': '454' + }, + { + 'name': 'Malaysia', + 'alpha2': 'MY', + 'alpha3': 'MYS', + 'numeric': '458' + }, + { + 'name': 'Maldives', + 'alpha2': 'MV', + 'alpha3': 'MDV', + 'numeric': '462' + }, + { + 'name': 'Mali', + 'alpha2': 'ML', + 'alpha3': 'MLI', + 'numeric': '466' + }, + { + 'name': 'Malta', + 'alpha2': 'MT', + 'alpha3': 'MLT', + 'numeric': '470' + }, + { + 'name': 'Marshall Islands (the)', + 'alpha2': 'MH', + 'alpha3': 'MHL', + 'numeric': '584', + 'altName': 'Marshall Islands' + }, + { + 'name': 'Martinique', + 'alpha2': 'MQ', + 'alpha3': 'MTQ', + 'numeric': '474' + }, + { + 'name': 'Mauritania', + 'alpha2': 'MR', + 'alpha3': 'MRT', + 'numeric': '478' + }, + { + 'name': 'Mauritius', + 'alpha2': 'MU', + 'alpha3': 'MUS', + 'numeric': '480' + }, + { + 'name': 'Mayotte', + 'alpha2': 'YT', + 'alpha3': 'MYT', + 'numeric': '175' + }, + { + 'name': 'Mexico', + 'alpha2': 'MX', + 'alpha3': 'MEX', + 'numeric': '484' + }, + { + 'name': 'Micronesia (Federated States of)', + 'alpha2': 'FM', + 'alpha3': 'FSM', + 'numeric': '583', + 'altName': 'Micronesia, Federated States of', + 'shortName': 'Micronesia' + }, + { + 'name': 'Moldova (the Republic of)', + 'alpha2': 'MD', + 'alpha3': 'MDA', + 'numeric': '498', + 'altName': 'Moldova' + }, + { + 'name': 'Monaco', + 'alpha2': 'MC', + 'alpha3': 'MCO', + 'numeric': '492' + }, + { + 'name': 'Mongolia', + 'alpha2': 'MN', + 'alpha3': 'MNG', + 'numeric': '496' + }, + { + 'name': 'Montenegro', + 'alpha2': 'ME', + 'alpha3': 'MNE', + 'numeric': '499' + }, + { + 'name': 'Montserrat', + 'alpha2': 'MS', + 'alpha3': 'MSR', + 'numeric': '500' + }, + { + 'name': 'Morocco', + 'alpha2': 'MA', + 'alpha3': 'MAR', + 'numeric': '504' + }, + { + 'name': 'Mozambique', + 'alpha2': 'MZ', + 'alpha3': 'MOZ', + 'numeric': '508' + }, + { + 'name': 'Myanmar', + 'alpha2': 'MM', + 'alpha3': 'MMR', + 'numeric': '104' + }, + { + 'name': 'Namibia', + 'alpha2': 'NA', + 'alpha3': 'NAM', + 'numeric': '516' + }, + { + 'name': 'Nauru', + 'alpha2': 'NR', + 'alpha3': 'NRU', + 'numeric': '520' + }, + { + 'name': 'Nepal', + 'alpha2': 'NP', + 'alpha3': 'NPL', + 'numeric': '524' + }, + { + 'name': 'Netherlands (the)', + 'alpha2': 'NL', + 'alpha3': 'NLD', + 'numeric': '528', + 'altName': 'Netherlands' + }, + { + 'name': 'New Caledonia', + 'alpha2': 'NC', + 'alpha3': 'NCL', + 'numeric': '540' + }, + { + 'name': 'New Zealand', + 'alpha2': 'NZ', + 'alpha3': 'NZL', + 'numeric': '554' + }, + { + 'name': 'Nicaragua', + 'alpha2': 'NI', + 'alpha3': 'NIC', + 'numeric': '558' + }, + { + 'name': 'Niger (the)', + 'alpha2': 'NE', + 'alpha3': 'NER', + 'numeric': '562', + 'altName': 'Niger' + }, + { + 'name': 'Nigeria', + 'alpha2': 'NG', + 'alpha3': 'NGA', + 'numeric': '566' + }, + { + 'name': 'Niue', + 'alpha2': 'NU', + 'alpha3': 'NIU', + 'numeric': '570' + }, + { + 'name': 'Norfolk Island', + 'alpha2': 'NF', + 'alpha3': 'NFK', + 'numeric': '574' + }, + { + 'name': 'Northern Mariana Islands (the)', + 'alpha2': 'MP', + 'alpha3': 'MNP', + 'numeric': '580', + 'altName': 'Northern Mariana Islands' + }, + { + 'name': 'Norway', + 'alpha2': 'NO', + 'alpha3': 'NOR', + 'numeric': '578' + }, + { + 'name': 'Oman', + 'alpha2': 'OM', + 'alpha3': 'OMN', + 'numeric': '512' + }, + { + 'name': 'Pakistan', + 'alpha2': 'PK', + 'alpha3': 'PAK', + 'numeric': '586' + }, + { + 'name': 'Palau', + 'alpha2': 'PW', + 'alpha3': 'PLW', + 'numeric': '585' + }, + { + 'name': 'Palestine, State of', + 'alpha2': 'PS', + 'alpha3': 'PSE', + 'numeric': '275', + 'altName': 'Palestinian Territory', + 'shortName': 'Palestine' + }, + { + 'name': 'Panama', + 'alpha2': 'PA', + 'alpha3': 'PAN', + 'numeric': '591' + }, + { + 'name': 'Papua New Guinea', + 'alpha2': 'PG', + 'alpha3': 'PNG', + 'numeric': '598' + }, + { + 'name': 'Paraguay', + 'alpha2': 'PY', + 'alpha3': 'PRY', + 'numeric': '600' + }, + { + 'name': 'Peru', + 'alpha2': 'PE', + 'alpha3': 'PER', + 'numeric': '604' + }, + { + 'name': 'Philippines (the)', + 'alpha2': 'PH', + 'alpha3': 'PHL', + 'numeric': '608', + 'altName': 'Philippines' + }, + { + 'name': 'Pitcairn', + 'alpha2': 'PN', + 'alpha3': 'PCN', + 'numeric': '612' + }, + { + 'name': 'Poland', + 'alpha2': 'PL', + 'alpha3': 'POL', + 'numeric': '616' + }, + { + 'name': 'Portugal', + 'alpha2': 'PT', + 'alpha3': 'PRT', + 'numeric': '620' + }, + { + 'name': 'Puerto Rico', + 'alpha2': 'PR', + 'alpha3': 'PRI', + 'numeric': '630' + }, + { + 'name': 'Qatar', + 'alpha2': 'QA', + 'alpha3': 'QAT', + 'numeric': '634' + }, + { + 'name': 'Réunion', + 'alpha2': 'RE', + 'alpha3': 'REU', + 'numeric': '638', + 'shortName': 'Reunion' + }, + { + 'name': 'Romania', + 'alpha2': 'RO', + 'alpha3': 'ROU', + 'numeric': '642' + }, + { + 'name': 'Russian Federation (the)', + 'alpha2': 'RU', + 'alpha3': 'RUS', + 'numeric': '643', + 'altName': 'Russian Federation', + 'shortName': 'Russia' + }, + { + 'name': 'Rwanda', + 'alpha2': 'RW', + 'alpha3': 'RWA', + 'numeric': '646' + }, + { + 'name': 'Saint Barthélemy', + 'alpha2': 'BL', + 'alpha3': 'BLM', + 'numeric': '652', + 'altName': 'Saint-Barthélemy', + 'shortName': 'Saint Barthelemy' + }, + { + 'name': 'Saint Helena, Ascension and Tristan da Cunha', + 'alpha2': 'SH', + 'alpha3': 'SHN', + 'numeric': '654', + 'altName': 'Saint Helena' + }, + { + 'name': 'Saint Kitts and Nevis', + 'alpha2': 'KN', + 'alpha3': 'KNA', + 'numeric': '659' + }, + { + 'name': 'Saint Lucia', + 'alpha2': 'LC', + 'alpha3': 'LCA', + 'numeric': '662' + }, + { + 'name': 'Saint Martin (French part)', + 'alpha2': 'MF', + 'alpha3': 'MAF', + 'numeric': '663', + 'altName': 'Saint-Martin (French part)', + 'shortName': 'Saint Martin' + }, + { + 'name': 'Saint Pierre and Miquelon', + 'alpha2': 'PM', + 'alpha3': 'SPM', + 'numeric': '666' + }, + { + 'name': 'Saint Vincent and the Grenadines', + 'alpha2': 'VC', + 'alpha3': 'VCT', + 'numeric': '670', + 'altName': 'Saint Vincent and Grenadines' + }, + { + 'name': 'Samoa', + 'alpha2': 'WS', + 'alpha3': 'WSM', + 'numeric': '882' + }, + { + 'name': 'San Marino', + 'alpha2': 'SM', + 'alpha3': 'SMR', + 'numeric': '674' + }, + { + 'name': 'Sao Tome and Principe', + 'alpha2': 'ST', + 'alpha3': 'STP', + 'numeric': '678' + }, + { + 'name': 'Saudi Arabia', + 'alpha2': 'SA', + 'alpha3': 'SAU', + 'numeric': '682' + }, + { + 'name': 'Senegal', + 'alpha2': 'SN', + 'alpha3': 'SEN', + 'numeric': '686' + }, + { + 'name': 'Serbia', + 'alpha2': 'RS', + 'alpha3': 'SRB', + 'numeric': '688' + }, + { + 'name': 'Seychelles', + 'alpha2': 'SC', + 'alpha3': 'SYC', + 'numeric': '690' + }, + { + 'name': 'Sierra Leone', + 'alpha2': 'SL', + 'alpha3': 'SLE', + 'numeric': '694' + }, + { + 'name': 'Singapore', + 'alpha2': 'SG', + 'alpha3': 'SGP', + 'numeric': '702' + }, + { + 'name': 'Sint Maarten (Dutch part)', + 'alpha2': 'SX', + 'alpha3': 'SXM', + 'numeric': '534', + 'shortName': 'Sint Maarten' + }, + { + 'name': 'Slovakia', + 'alpha2': 'SK', + 'alpha3': 'SVK', + 'numeric': '703' + }, + { + 'name': 'Slovenia', + 'alpha2': 'SI', + 'alpha3': 'SVN', + 'numeric': '705' + }, + { + 'name': 'Solomon Islands', + 'alpha2': 'SB', + 'alpha3': 'SLB', + 'numeric': '090' + }, + { + 'name': 'Somalia', + 'alpha2': 'SO', + 'alpha3': 'SOM', + 'numeric': '706' + }, + { + 'name': 'South Africa', + 'alpha2': 'ZA', + 'alpha3': 'ZAF', + 'numeric': '710' + }, + { + 'name': 'South Georgia and the South Sandwich Islands', + 'alpha2': 'GS', + 'alpha3': 'SGS', + 'numeric': '239' + }, + { + 'name': 'South Sudan', + 'alpha2': 'SS', + 'alpha3': 'SSD', + 'numeric': '728' + }, + { + 'name': 'Spain', + 'alpha2': 'ES', + 'alpha3': 'ESP', + 'numeric': '724' + }, + { + 'name': 'Sri Lanka', + 'alpha2': 'LK', + 'alpha3': 'LKA', + 'numeric': '144' + }, + { + 'name': 'Sudan (the)', + 'alpha2': 'SD', + 'alpha3': 'SDN', + 'numeric': '729', + 'altName': 'Sudan' + }, + { + 'name': 'Suriname', + 'alpha2': 'SR', + 'alpha3': 'SUR', + 'numeric': '740' + }, + { + 'name': 'Svalbard and Jan Mayen', + 'alpha2': 'SJ', + 'alpha3': 'SJM', + 'numeric': '744', + 'altName': 'Svalbard and Jan Mayen Islands' + }, + { + 'name': 'Swaziland', + 'alpha2': 'SZ', + 'alpha3': 'SWZ', + 'numeric': '748' + }, + { + 'name': 'Sweden', + 'alpha2': 'SE', + 'alpha3': 'SWE', + 'numeric': '752' + }, + { + 'name': 'Switzerland', + 'alpha2': 'CH', + 'alpha3': 'CHE', + 'numeric': '756' + }, + { + 'name': 'Syrian Arab Republic', + 'alpha2': 'SY', + 'alpha3': 'SYR', + 'numeric': '760', + 'altName': 'Syrian Arab Republic (Syria)', + 'shortName': 'Syria' + }, + { + 'name': 'Taiwan (Province of China)', + 'alpha2': 'TW', + 'alpha3': 'TWN', + 'numeric': '158', + 'altName': 'Taiwan, Republic of China', + 'shortName': 'Taiwan' + }, + { + 'name': 'Tajikistan', + 'alpha2': 'TJ', + 'alpha3': 'TJK', + 'numeric': '762' + }, + { + 'name': 'Tanzania, United Republic of', + 'alpha2': 'TZ', + 'alpha3': 'TZA', + 'numeric': '834', + 'shortName': 'Tanzania' + }, + { + 'name': 'Thailand', + 'alpha2': 'TH', + 'alpha3': 'THA', + 'numeric': '764' + }, + { + 'name': 'Timor-Leste', + 'alpha2': 'TL', + 'alpha3': 'TLS', + 'numeric': '626', + 'shortName': 'East Timor' + }, + { + 'name': 'Togo', + 'alpha2': 'TG', + 'alpha3': 'TGO', + 'numeric': '768' + }, + { + 'name': 'Tokelau', + 'alpha2': 'TK', + 'alpha3': 'TKL', + 'numeric': '772' + }, + { + 'name': 'Tonga', + 'alpha2': 'TO', + 'alpha3': 'TON', + 'numeric': '776' + }, + { + 'name': 'Trinidad and Tobago', + 'alpha2': 'TT', + 'alpha3': 'TTO', + 'numeric': '780' + }, + { + 'name': 'Tunisia', + 'alpha2': 'TN', + 'alpha3': 'TUN', + 'numeric': '788' + }, + { + 'name': 'Turkey', + 'alpha2': 'TR', + 'alpha3': 'TUR', + 'numeric': '792' + }, + { + 'name': 'Turkmenistan', + 'alpha2': 'TM', + 'alpha3': 'TKM', + 'numeric': '795' + }, + { + 'name': 'Turks and Caicos Islands (the)', + 'alpha2': 'TC', + 'alpha3': 'TCA', + 'numeric': '796', + 'altName': 'Turks and Caicos Islands' + }, + { + 'name': 'Tuvalu', + 'alpha2': 'TV', + 'alpha3': 'TUV', + 'numeric': '798' + }, + { + 'name': 'Uganda', + 'alpha2': 'UG', + 'alpha3': 'UGA', + 'numeric': '800' + }, + { + 'name': 'Ukraine', + 'alpha2': 'UA', + 'alpha3': 'UKR', + 'numeric': '804' + }, + { + 'name': 'United Arab Emirates (the)', + 'alpha2': 'AE', + 'alpha3': 'ARE', + 'numeric': '784', + 'altName': 'United Arab Emirates' + }, + { + 'name': 'United Kingdom of Great Britain and Northern Ireland (the)', + 'alpha2': 'GB', + 'alpha3': 'GBR', + 'numeric': '826', + 'altName': 'United Kingdom' + }, + { + 'name': 'United States Minor Outlying Islands (the)', + 'alpha2': 'UM', + 'alpha3': 'UMI', + 'numeric': '581', + 'altName': 'US Minor Outlying Islands' + }, + { + 'name': 'United States of America (the)', + 'alpha2': 'US', + 'alpha3': 'USA', + 'numeric': '840', + 'altName': 'United States of America', + 'shortName': 'United States' + }, + { + 'name': 'Uruguay', + 'alpha2': 'UY', + 'alpha3': 'URY', + 'numeric': '858' + }, + { + 'name': 'Uzbekistan', + 'alpha2': 'UZ', + 'alpha3': 'UZB', + 'numeric': '860' + }, + { + 'name': 'Vanuatu', + 'alpha2': 'VU', + 'alpha3': 'VUT', + 'numeric': '548' + }, + { + 'name': 'Venezuela (Bolivarian Republic of)', + 'alpha2': 'VE', + 'alpha3': 'VEN', + 'numeric': '862', + 'altName': 'Venezuela (Bolivarian Republic)', + 'shortName': 'Venezuela' + }, + { + 'name': 'Viet Nam', + 'alpha2': 'VN', + 'alpha3': 'VNM', + 'numeric': '704', + 'shortName': 'Vietnam' + }, + { + 'name': 'Virgin Islands (British)', + 'alpha2': 'VG', + 'alpha3': 'VGB', + 'numeric': '092', + 'altName': 'British Virgin Islands' + }, + { + 'name': 'Virgin Islands (U.S.)', + 'alpha2': 'VI', + 'alpha3': 'VIR', + 'numeric': '850', + 'altName': 'Virgin Islands, US', + 'shortName': 'U.S. Virgin Islands' + }, + { + 'name': 'Wallis and Futuna', + 'alpha2': 'WF', + 'alpha3': 'WLF', + 'numeric': '876', + 'altName': 'Wallis and Futuna Islands' + }, + { + 'name': 'Western Sahara*', + 'alpha2': 'EH', + 'alpha3': 'ESH', + 'numeric': '732', + 'altName': 'Western Sahara' + }, + { + 'name': 'Yemen', + 'alpha2': 'YE', + 'alpha3': 'YEM', + 'numeric': '887' + }, + { + 'name': 'Zambia', + 'alpha2': 'ZM', + 'alpha3': 'ZMB', + 'numeric': '894' + }, + { + 'name': 'Zimbabwe', + 'alpha2': 'ZW', + 'alpha3': 'ZWE', + 'numeric': '716' + } +]; diff --git a/packages/nodes-base/nodes/Google/YouTube/GenericFunctions.ts b/packages/nodes-base/nodes/Google/YouTube/GenericFunctions.ts index 174f5755d5..65ed4f182c 100644 --- a/packages/nodes-base/nodes/Google/YouTube/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/YouTube/GenericFunctions.ts @@ -29,14 +29,26 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF if (Object.keys(body).length === 0) { delete options.body; } + //@ts-ignore return await this.helpers.requestOAuth2.call(this, 'youTubeOAuth2Api', options); } catch (error) { - if (error.response && error.response.body && error.response.body.error) { - let errors = error.response.body.error.errors; + let errors; + + if (error.response && error.response.body) { + + if (resource === '/upload/youtube/v3/videos') { + error.response.body = JSON.parse(error.response.body); + } + + if (error.response.body.error) { + + errors = error.response.body.error.errors; + + errors = errors.map((e: IDataObject) => e.message); + } - errors = errors.map((e: IDataObject) => e.message); // Try to return the error prettier throw new Error( `YouTube error response [${error.statusCode}]: ${errors.join('|')}` diff --git a/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts b/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts new file mode 100644 index 0000000000..55b90fb63b --- /dev/null +++ b/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts @@ -0,0 +1,449 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const playlistOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'playlist', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a playlist', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a playlist', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all playlists', + }, + { + name: 'Update', + value: 'update', + description: 'Update a playlist', + }, + ], + default: 'getAll', + description: 'The operation to perform.' + } +] as INodeProperties[]; + +export const playlistFields = [ + /* -------------------------------------------------------------------------- */ + /* playlist:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'playlist', + ], + }, + }, + default: '', + description: `The playlist's title.`, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'playlist', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: `The playlist's description.`, + }, + { + displayName: 'Privacy Status', + name: 'privacyStatus', + type: 'options', + options: [ + { + name: 'Private', + value: 'private', + }, + { + name: 'Public', + value: 'public', + }, + { + name: 'Unlisted', + value: 'unlistef', + }, + ], + default: '', + description: `The playlist's privacy status.`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: `Keyword tags associated with the playlist. Mulplie can be defined separated by comma`, + }, + { + displayName: 'Default Language', + name: 'defaultLanguage', + type: 'string', + default: '', + description: `The language of the text in the playlist resource's title and description properties.`, + }, + { + displayName: 'On Behalf Of Content Owner Channel', + name: 'onBehalfOfContentOwnerChannel', + type: 'string', + default: '', + description: `The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel to which a video is being added.
+ This parameter is required when a request specifies a value for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that parameter.`, + }, + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* playlist:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Playlist ID', + name: 'playlistId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'playlist', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'playlist', + ], + }, + }, + options: [ + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* playlist:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Fields', + name: 'part', + type: 'multiOptions', + options: [ + { + name: 'Content Details', + value: 'contentDetails', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Localizations', + value: 'localizations', + }, + { + name: 'Player', + value: 'player', + }, + { + name: 'Snippet', + value: 'snippet', + }, + { + name: 'Status', + value: 'status', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlist', + ], + }, + }, + description: 'The fields parameter specifies a comma-separated list of one or more playlist resource properties that the API response will include.', + default: '' + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlist', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlist', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlist', + ], + }, + }, + options: [ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + description: `This value indicates that the API should only return the specified channel's playlists.`, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: `The id parameter specifies a comma-separated list of the YouTube playlist ID(s) for the resource(s) that are being retrieved. In a playlist resource, the id property specifies the playlist's YouTube playlist ID.`, + }, + { + displayName: 'Mine', + name: 'mine', + type: 'boolean', + default: false, + description: `Set this parameter's value to true to instruct the API to only return playlists owned by the authenticated user.`, + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlist', + ], + }, + }, + options: [ + { + displayName: 'On Behalf Of Content Owner Channel', + name: 'onBehalfOfContentOwnerChannel', + type: 'string', + default: '', + description: `The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel to which a video is being added.
+ This parameter is required when a request specifies a value for the onBehalfOfContentOwner parameter, and it can only be used in conjunction with that parameter.`, + }, + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* playlist:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Playlist ID', + name: 'playlistId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'playlist', + ], + }, + }, + default: '', + description: `The playlist's title.`, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'playlist', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: `The playlist's description.`, + }, + { + displayName: 'Privacy Status', + name: 'privacyStatus', + type: 'options', + options: [ + { + name: 'Private', + value: 'private', + }, + { + name: 'Public', + value: 'public', + }, + { + name: 'Unlisted', + value: 'unlistef', + }, + ], + default: '', + description: `The playlist's privacy status.`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: `Keyword tags associated with the playlist. Mulplie can be defined separated by comma`, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + { + displayName: 'Default Language', + name: 'defaultLanguage', + type: 'string', + default: '', + description: `The language of the text in the playlist resource's title and description properties.`, + }, + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts new file mode 100644 index 0000000000..b7c4f9d294 --- /dev/null +++ b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts @@ -0,0 +1,862 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const videoOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'video', + ], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a video', + }, + { + name: 'Get', + value: 'get', + description: 'Get a video', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all videos', + }, + { + name: 'Rate', + value: 'rate', + description: 'Rate a video', + }, + { + name: 'Update', + value: 'update', + description: 'Update a video', + }, + { + name: 'Upload', + value: 'upload', + description: 'Upload a video', + }, + ], + default: 'getAll', + description: 'The operation to perform.' + } +] as INodeProperties[]; + +export const videoFields = [ + /* -------------------------------------------------------------------------- */ + /* video:upload */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'video', + ], + }, + }, + default: '', + }, + { + displayName: 'Country Code', + name: 'countryCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCountriesCodes', + }, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'video', + ], + }, + }, + default: '', + }, + { + displayName: 'Category ID', + name: 'categoryId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getVideoCategories', + loadOptionsDependsOn: [ + 'countryCode', + ], + }, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'video', + ], + }, + }, + default: '', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'video', + ], + }, + }, + default: 'data', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'video', + ], + }, + }, + options: [ + { + displayName: 'Default Language', + name: 'defaultLanguage', + type: 'string', + default: '', + description: `The language of the text in the playlist resource's title and description properties.`, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: `The playlist's description.`, + }, + { + displayName: 'Embeddable', + name: 'embeddable', + type: 'boolean', + default: false, + description: `This value indicates whether the video can be embedded on another website.`, + }, + { + displayName: 'License', + name: 'license', + type: 'options', + options: [ + { + name: 'Creative Common', + value: 'creativeCommon', + }, + { + name: 'Youtube', + value: 'youtube', + }, + ], + default: false, + description: `The video's license.`, + }, + { + displayName: 'Notify Subscribers', + name: 'notifySubscribers', + type: 'boolean', + default: false, + description: `The notifySubscribers parameter indicates whether YouTube should send a notification about the new video to users who subscribe to the video's channel`, + }, + { + displayName: 'Privacy Status', + name: 'privacyStatus', + type: 'options', + options: [ + { + name: 'Private', + value: 'private', + }, + { + name: 'Public', + value: 'public', + }, + { + name: 'Unlisted', + value: 'unlistef', + }, + ], + default: '', + description: `The playlist's privacy status.`, + }, + { + displayName: 'Public Stats Viewable', + name: 'publicStatsViewable', + type: 'boolean', + default: true, + description: `This value indicates whether the extended video statistics on the video's watch page are publicly viewable.`, + }, + { + displayName: 'Publish At', + name: 'publishAt', + type: 'dateTime', + default: '', + description: `If you set a value for this property, you must also set the status.privacyStatus property to private.`, + }, + { + displayName: 'Recording Date', + name: 'recordingDate', + type: 'dateTime', + default: '', + description: `The date and time when the video was recorded`, + }, + { + displayName: 'Self Declared Made For Kids', + name: 'selfDeclaredMadeForKids', + type: 'boolean', + default: false, + description: `This value indicates whether the video is designated as child-directed, and it contains the current "made for kids" status of the video`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: `Keyword tags associated with the playlist. Mulplie can be defined separated by comma`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* video:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Video ID', + name: 'videoId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'video', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'video', + ], + }, + }, + options: [ + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* video:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Video ID', + name: 'videoId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'video', + ], + }, + }, + default: '' + }, + { + displayName: 'Fields', + name: 'part', + type: 'multiOptions', + options: [ + { + name: 'Content Details', + value: 'contentDetails', + }, + { + name: 'Field Details', + value: 'fieldDetails', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Live Streaming Details', + value: 'liveStreamingDetails', + }, + { + name: 'Localizations', + value: 'localizations', + }, + { + name: 'Player', + value: 'player', + }, + { + name: 'Processing Details', + value: 'processingDetails', + }, + { + name: 'Recording Details', + value: 'recordingDetails', + }, + { + name: 'Snippet', + value: 'snippet', + }, + { + name: 'Statistics', + value: 'statistics', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Suggestions', + value: 'suggestions', + }, + { + name: 'Topic Details', + value: 'topicDetails', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'video', + ], + }, + }, + description: 'The fields parameter specifies a comma-separated list of one or more video resource properties that the API response will include.', + default: '' + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'video', + ], + }, + }, + options: [ + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* video:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Fields', + name: 'part', + type: 'multiOptions', + options: [ + { + name: 'Content Details', + value: 'contentDetails', + }, + { + name: 'File Details', + value: 'fileDetails', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Live Streaming Details', + value: 'liveStreamingDetails', + }, + { + name: 'Localizations', + value: 'localizations', + }, + { + name: 'Player', + value: 'player', + }, + { + name: 'Processing Details', + value: 'processingDetails', + }, + { + name: 'Recording Details', + value: 'recordingDetails', + }, + { + name: 'Snippet', + value: 'snippet', + }, + { + name: 'Statistics', + value: 'statistics', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Suggestions', + value: 'suggestions', + }, + { + name: 'Topic Details', + value: 'topicDetails', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'video', + ], + }, + }, + description: 'The fields parameter specifies a comma-separated list of one or more video resource properties that the API response will include.', + default: '' + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'video', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'video', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'video', + ], + }, + }, + options: [ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: `The id parameter specifies a comma-separated list of the YouTube video ID(s) for the resource(s) that are being retrieved. In a video resource, the id property specifies the video's YouTube video ID.`, + }, + { + displayName: 'My Rating', + name: 'myRating', + type: 'options', + options: [ + { + name: 'Dislike', + value: 'dislike', + }, + { + name: 'Like', + value: 'like', + }, + ], + default: '', + description: `Set this parameter's value to like or dislike to instruct the API to only return videos liked or disliked by the authenticated user.`, + }, + { + displayName: 'Region Code', + name: 'regionCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCountriesCodes', + }, + default: '', + description: `The regionCode parameter instructs the API to select a video chart available in the specified region.`, + }, + { + displayName: 'Video Category ID', + name: 'videoCategoryId', + type: 'string', + default: '', + description: `The videoCategoryId parameter identifies the video category for which the chart should be retrieved.`, + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'video', + ], + }, + }, + options: [ + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* video:rate */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Video ID', + name: 'videoId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'rate', + ], + resource: [ + 'video', + ], + }, + }, + default: '', + }, + { + displayName: 'Rating', + name: 'rating', + type: 'options', + displayOptions: { + show: { + operation: [ + 'rate', + ], + resource: [ + 'video', + ], + }, + }, + options: [ + { + name: 'Dislike', + value: 'dislike', + description: 'Records that the authenticated user disliked the video.', + }, + { + name: 'Like', + value: 'like', + description: 'Records that the authenticated user liked the video.', + }, + { + name: 'None', + value: 'none', + description: 'Removes any rating that the authenticated user had previously set for the video.', + }, + ], + default: '', + }, + /* -------------------------------------------------------------------------- */ + /* video:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Video ID', + name: 'videoId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'video', + ], + }, + }, + default: '', + }, + // { + // displayName: 'Country Code', + // name: 'countryCode', + // type: 'options', + // typeOptions: { + // loadOptionsMethod: 'getCountriesCodes', + // }, + // displayOptions: { + // show: { + // operation: [ + // 'update', + // ], + // resource: [ + // 'video', + // ], + // }, + // }, + // default: '', + // }, + // { + // displayName: 'Category ID', + // name: 'categoryId', + // type: 'options', + // typeOptions: { + // loadOptionsMethod: 'getVideoCategories', + // loadOptionsDependsOn: [ + // 'countryCode', + // ], + // }, + // displayOptions: { + // show: { + // operation: [ + // 'update', + // ], + // resource: [ + // 'video', + // ], + // }, + // }, + // default: '', + // }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'video', + ], + }, + }, + options: [ + { + displayName: 'Default Language', + name: 'defaultLanguage', + type: 'string', + default: '', + description: `The language of the text in the playlist resource's title and description properties.`, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: `The playlist's description.`, + }, + { + displayName: 'Embeddable', + name: 'embeddable', + type: 'boolean', + default: false, + description: `This value indicates whether the video can be embedded on another website.`, + }, + { + displayName: 'License', + name: 'license', + type: 'options', + options: [ + { + name: 'Creative Common', + value: 'creativeCommon', + }, + { + name: 'Youtube', + value: 'youtube', + }, + ], + default: false, + description: `The video's license.`, + }, + { + displayName: 'Notify Subscribers', + name: 'notifySubscribers', + type: 'boolean', + default: false, + description: `The notifySubscribers parameter indicates whether YouTube should send a notification about the new video to users who subscribe to the video's channel`, + }, + { + displayName: 'Privacy Status', + name: 'privacyStatus', + type: 'options', + options: [ + { + name: 'Private', + value: 'private', + }, + { + name: 'Public', + value: 'public', + }, + { + name: 'Unlisted', + value: 'unlistef', + }, + ], + default: '', + description: `The playlist's privacy status.`, + }, + { + displayName: 'Public Stats Viewable', + name: 'publicStatsViewable', + type: 'boolean', + default: true, + description: `This value indicates whether the extended video statistics on the video's watch page are publicly viewable.`, + }, + { + displayName: 'Publish At', + name: 'publishAt', + type: 'dateTime', + default: '', + description: `If you set a value for this property, you must also set the status.privacyStatus property to private.`, + }, + { + displayName: 'Recording Date', + name: 'recordingDate', + type: 'dateTime', + default: '', + description: `The date and time when the video was recorded`, + }, + { + displayName: 'Self Declared Made For Kids', + name: 'selfDeclaredMadeForKids', + type: 'boolean', + default: false, + description: `This value indicates whether the video is designated as child-directed, and it contains the current "made for kids" status of the video`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + default: '', + description: `Keyword tags associated with the playlist. Mulplie can be defined separated by comma`, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts index 4621c06d5b..fe9c8d5309 100644 --- a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts +++ b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts @@ -1,5 +1,6 @@ import { - IExecuteFunctions, BINARY_ENCODING, + IExecuteFunctions, + BINARY_ENCODING, } from 'n8n-core'; import { @@ -21,6 +22,20 @@ import { channelFields, } from './ChannelDescription'; +import { + playlistOperations, + playlistFields, +} from './PlaylistDescription'; + +import { + videoOperations, + videoFields, +} from './VideoDescription'; + +import { + countriesCodes, +} from './CountryCodes'; + export class YouTube implements INodeType { description: INodeTypeDescription = { displayName: 'Youtube', @@ -52,12 +67,26 @@ export class YouTube implements INodeType { name: 'Channel', value: 'channel', }, + { + name: 'Playlist', + value: 'playlist', + }, + { + name: 'Video', + value: 'video', + }, ], default: 'channel', description: 'The resource to operate on.' }, ...channelOperations, ...channelFields, + + ...playlistOperations, + ...playlistFields, + + ...videoOperations, + ...videoFields, ], }; @@ -85,6 +114,48 @@ export class YouTube implements INodeType { } return returnData; }, + // Get all the countries codes to display them to user so that he can + // select them easily + async getCountriesCodes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const countryCode of countriesCodes) { + const countryCodeName = `${countryCode.name} - ${countryCode.alpha2}`; + const countryCodeId = countryCode.alpha2; + returnData.push({ + name: countryCodeName, + value: countryCodeId, + }); + } + return returnData; + }, + // Get all the video categories to display them to user so that he can + // select them easily + async getVideoCategories( + this: ILoadOptionsFunctions + ): Promise { + const countryCode = this.getCurrentNodeParameter('countryCode') as string; + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = {}; + qs.regionCode = countryCode; + qs.part = 'snippet'; + const categories = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + '/youtube/v3/videoCategories', + {}, + qs, + ); + for (const category of categories) { + const categoryName = category.snippet.title; + const categoryId = category.id; + returnData.push({ + name: categoryName, + value: categoryId + }); + } + return returnData; + }, } }; @@ -103,23 +174,24 @@ export class YouTube implements INodeType { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const part = this.getNodeParameter('part', i) as string[]; const options = this.getNodeParameter('options', i) as IDataObject; + const filters = this.getNodeParameter('filters', i) as IDataObject; qs.part = part.join(','); - if (options.categoryId) { - qs.categoryId = options.categoryId as string; + if (filters.categoryId) { + qs.categoryId = filters.categoryId as string; } - if (options.forUsername) { - qs.forUsername = options.forUsername as string; + if (filters.forUsername) { + qs.forUsername = filters.forUsername as string; } - if (options.id) { - qs.id = options.id as string; + if (filters.id) { + qs.id = filters.id as string; } - if (options.managedByMe) { - qs.managedByMe = options.managedByMe as boolean; + if (filters.managedByMe) { + qs.managedByMe = filters.managedByMe as boolean; } - if (options.mine) { - qs.mine = options.mine as boolean; + if (filters.mine) { + qs.mine = filters.mine as boolean; } if (options.h1) { qs.h1 = options.h1 as string; @@ -298,6 +370,446 @@ export class YouTube implements INodeType { ); } } + if (resource === 'playlist') { + //https://developers.google.com/youtube/v3/docs/playlists/list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const part = this.getNodeParameter('part', i) as string[]; + const options = this.getNodeParameter('options', i) as IDataObject; + const filters = this.getNodeParameter('filters', i) as IDataObject; + + qs.part = part.join(','); + + Object.assign(qs, options, filters); + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + `/youtube/v3/playlists`, + {}, + qs + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/youtube/v3/playlists`, + {}, + qs + ); + responseData = responseData.items; + } + } + //https://developers.google.com/youtube/v3/docs/playlists/insert + if (operation === 'create') { + const title = this.getNodeParameter('title', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + qs.part = 'snippet'; + + const body: IDataObject = { + snippet: { + title, + }, + }; + + if (options.tags) { + //@ts-ignore + body.snippet.tags = (options.tags as string).split(',') as string[]; + } + + if (options.description) { + //@ts-ignore + body.snippet.privacyStatus = options.privacyStatus as string; + } + + if (options.defaultLanguage) { + //@ts-ignore + body.snippet.defaultLanguage = options.defaultLanguage as string; + } + + if (options.onBehalfOfContentOwner) { + qs.onBehalfOfContentOwner = options.onBehalfOfContentOwner as string; + } + + if (options.onBehalfOfContentOwnerChannel) { + qs.onBehalfOfContentOwnerChannel = options.onBehalfOfContentOwnerChannel as string; + } + + responseData = await googleApiRequest.call( + this, + 'POST', + '/youtube/v3/playlists', + body, + qs + ); + } + //https://developers.google.com/youtube/v3/docs/playlists/update + if (operation === 'update') { + const playlistId = this.getNodeParameter('playlistId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + qs.part = 'snippet'; + + const body: IDataObject = { + id: playlistId, + snippet: { + } + }; + + if (updateFields.tags) { + //@ts-ignore + body.snippet.tags = (updateFields.tags as string).split(',') as string[]; + } + + if (updateFields.title) { + //@ts-ignore + body.snippet.title = updateFields.title as string + } + + if (updateFields.description) { + //@ts-ignore + body.snippet.description = updateFields.description as string; + } + + if (updateFields.defaultLanguage) { + //@ts-ignore + body.snippet.defaultLanguage = updateFields.defaultLanguage as string; + } + + if (updateFields.onBehalfOfContentOwner) { + qs.onBehalfOfContentOwner = updateFields.onBehalfOfContentOwner as string; + } + + responseData = await googleApiRequest.call( + this, + 'PUT', + '/youtube/v3/playlists', + body, + qs + ); + } + //https://developers.google.com/youtube/v3/docs/playlists/delete + if (operation === 'delete') { + const playlistId = this.getNodeParameter('playlistId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + const body: IDataObject = { + id: playlistId, + }; + + if (options.onBehalfOfContentOwner) { + qs.onBehalfOfContentOwner = options.onBehalfOfContentOwner as string; + } + + responseData = await googleApiRequest.call( + this, + 'DELETE', + '/youtube/v3/playlists', + body, + ); + + responseData = { success: true }; + } + } + if (resource === 'video') { + //https://developers.google.com/youtube/v3/docs/videos/list?hl=en + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const part = this.getNodeParameter('part', i) as string[]; + const options = this.getNodeParameter('options', i) as IDataObject; + const filters = this.getNodeParameter('filters', i) as IDataObject; + + qs.part = part.join(','); + + qs.chart = 'mostPopular'; + + Object.assign(qs, options, filters); + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + `/youtube/v3/videos`, + {}, + qs + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/youtube/v3/videos`, + {}, + qs + ); + responseData = responseData.items; + } + } + //https://developers.google.com/youtube/v3/docs/videos/list?hl=en + if (operation === 'get') { + const part = this.getNodeParameter('part', i) as string[]; + const videoId = this.getNodeParameter('videoId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + qs.part = part.join(','); + + qs.id = videoId; + + Object.assign(qs, options); + + responseData = await googleApiRequest.call( + this, + 'GET', + `/youtube/v3/videos`, + {}, + qs + ); + + responseData = responseData.items; + } + //https://developers.google.com/youtube/v3/guides/uploading_a_video?hl=en + if (operation === 'upload') { + const title = this.getNodeParameter('title', i) as string; + const categoryId = this.getNodeParameter('categoryId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + + let mimeType; + + // Is binary file to upload + const item = items[i]; + + if (item.binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + if (item.binary[binaryProperty] === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + if (item.binary[binaryProperty].mimeType) { + mimeType = item.binary[binaryProperty].mimeType; + } + + const body = Buffer.from(item.binary[binaryProperty].data, BINARY_ENCODING); + + const requestOptions = { + headers: { + 'Content-Type': mimeType, + }, + json: false, + }; + + let response = await googleApiRequest.call(this, 'POST', '/upload/youtube/v3/videos', body, qs, undefined, requestOptions); + + const { id } = JSON.parse(response); + + qs.part = 'snippet, status, recordingDetails'; + + const data = { + id, + snippet: { + title, + categoryId, + }, + status: { + }, + recordingDetails: { + }, + } + + if (options.description) { + //@ts-ignore + data.snippet.description = options.description as string; + } + + if (options.privacyStatus) { + //@ts-ignore + data.status.privacyStatus = options.privacyStatus as string; + } + + if (options.tags) { + //@ts-ignore + data.snippet.tags = (options.tags as string).split(',') as string[]; + } + + if (options.embeddable) { + //@ts-ignore + data.status.embeddable = options.embeddable as boolean; + } + + if (options.publicStatsViewable) { + //@ts-ignore + data.status.publicStatsViewable = options.publicStatsViewable as boolean; + } + + if (options.publishAt) { + //@ts-ignore + data.status.publishAt = options.publishAt as string; + } + + if (options.recordingDate) { + //@ts-ignore + data.recordingDetails.recordingDate = options.recordingDate as string; + } + + if (options.selfDeclaredMadeForKids) { + //@ts-ignore + data.status.selfDeclaredMadeForKids = options.selfDeclaredMadeForKids as boolean; + } + + if (options.license) { + //@ts-ignore + data.status.license = options.license as string; + } + + if (options.defaultLanguage) { + //@ts-ignore + data.snippet.defaultLanguage = options.defaultLanguage as string; + } + + if (options.notifySubscribers) { + qs.notifySubscribers = options.notifySubscribers; + delete options.notifySubscribers; + } + + responseData = await googleApiRequest.call( + this, + 'PUT', + `/youtube/v3/videos`, + data, + qs, + ); + } + //https://developers.google.com/youtube/v3/docs/playlists/update + if (operation === 'update') { + const id = this.getNodeParameter('videoId', i) as string; + //const categoryId = this.getNodeParameter('categoryId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + qs.part = 'snippet, status, recordingDetails'; + + const body = { + id, + snippet: { + //categoryId, + }, + status: { + }, + recordingDetails: { + }, + } + + if (updateFields.description) { + //@ts-ignore + data.snippet.description = updateFields.description as string; + } + + if (updateFields.privacyStatus) { + //@ts-ignore + data.status.privacyStatus = updateFields.privacyStatus as string; + } + + if (updateFields.tags) { + //@ts-ignore + data.snippet.tags = (updateFields.tags as string).split(',') as string[]; + } + + if (updateFields.description) { + //@ts-ignore + data.snippet.title = updateFields.title as string; + } + + if (updateFields.embeddable) { + //@ts-ignore + data.status.embeddable = updateFields.embeddable as boolean; + } + + if (updateFields.publicStatsViewable) { + //@ts-ignore + data.status.publicStatsViewable = updateFields.publicStatsViewable as boolean; + } + + if (updateFields.publishAt) { + //@ts-ignore + data.status.publishAt = updateFields.publishAt as string; + } + + if (updateFields.recordingDate) { + //@ts-ignore + data.recordingDetails.recordingDate = updateFields.recordingDate as string; + } + + if (updateFields.selfDeclaredMadeForKids) { + //@ts-ignore + data.status.selfDeclaredMadeForKids = updateFields.selfDeclaredMadeForKids as boolean; + } + + if (updateFields.license) { + //@ts-ignore + data.status.license = options.license as string; + } + + if (updateFields.defaultLanguage) { + //@ts-ignore + data.snippet.defaultLanguage = updateFields.defaultLanguage as string; + } + + responseData = await googleApiRequest.call( + this, + 'PUT', + '/youtube/v3/videos', + body, + qs + ); + } + //https://developers.google.com/youtube/v3/docs/videos/delete?hl=en + if (operation === 'delete') { + const videoId = this.getNodeParameter('videoId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + const body: IDataObject = { + id: videoId, + }; + + if (options.onBehalfOfContentOwner) { + qs.onBehalfOfContentOwner = options.onBehalfOfContentOwner as string; + } + + responseData = await googleApiRequest.call( + this, + 'DELETE', + '/youtube/v3/video', + body, + ); + + responseData = { success: true }; + } + //https://developers.google.com/youtube/v3/docs/videos/rate?hl=en + if (operation === 'rate') { + const videoId = this.getNodeParameter('videoId', i) as string; + const rating = this.getNodeParameter('rating', i) as string; + + const body: IDataObject = { + id: videoId, + rating, + }; + + responseData = await googleApiRequest.call( + this, + 'POST', + '/youtube/v3/videos/rate', + body, + ); + + responseData = { success: true }; + } + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); From 8c24858f99d7d0b3c38d9201ec69d74b203bded8 Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 11 Aug 2020 16:07:23 -0400 Subject: [PATCH 4/6] :zap: Improvements --- .../Google/YouTube/ChannelDescription.ts | 92 ++++++++-- .../Google/YouTube/PlaylistDescription.ts | 172 +++++++++++++++--- .../YouTube/VideoCategoryDescription.ts | 95 ++++++++++ .../nodes/Google/YouTube/VideoDescription.ts | 141 +++++++------- .../nodes/Google/YouTube/YouTube.node.ts | 171 ++++++++++++----- 5 files changed, 515 insertions(+), 156 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/YouTube/VideoCategoryDescription.ts diff --git a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts index 6db5d3a990..5d08941aeb 100644 --- a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts @@ -15,6 +15,11 @@ export const channelOperations = [ }, }, options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve a channel', + }, { name: 'Get All', value: 'getAll', @@ -45,10 +50,6 @@ export const channelFields = [ name: 'part', type: 'multiOptions', options: [ - { - name: 'Audit Details', - value: 'auditDetails', - }, { name: 'Branding Settings', value: 'brandingSettings', @@ -186,13 +187,6 @@ export const channelFields = [ default: false, description: `Set this parameter's value to true to instruct the API to only return channels managed by the content owner that the onBehalfOfContentOwner parameter specifies`, }, - { - displayName: 'Mine', - name: 'mine', - type: 'boolean', - default: false, - description: `This parameter can only be used in a properly authorized request. Set this parameter's value to true to instruct the API to only return channels owned by the authenticated user.`, - }, ], }, { @@ -232,6 +226,82 @@ export const channelFields = [ ], }, /* -------------------------------------------------------------------------- */ + /* channel:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'channel', + ], + }, + }, + default: '', + }, + { + displayName: 'Fields', + name: 'part', + type: 'multiOptions', + options: [ + { + name: 'Branding Settings', + value: 'brandingSettings', + }, + { + name: 'Content Details', + value: 'contentDetails', + }, + { + name: 'Content Owner Details', + value: 'contentOwnerDetails', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Localizations', + value: 'localizations', + }, + { + name: 'Snippet', + value: 'snippet', + }, + { + name: 'Statistics', + value: 'statistics', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Topic Details', + value: 'topicDetails', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'channel', + ], + }, + }, + description: 'The fields parameter specifies a comma-separated list of one or more channel resource properties that the API response will include.', + default: '' + }, + /* -------------------------------------------------------------------------- */ /* channel:update */ /* -------------------------------------------------------------------------- */ { diff --git a/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts b/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts index 55b90fb63b..5cb58ee97b 100644 --- a/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts @@ -25,6 +25,11 @@ export const playlistOperations = [ value: 'delete', description: 'Delete a playlist', }, + { + name: 'Get', + value: 'get', + description: 'Get a playlist', + }, { name: 'Get All', value: 'getAll', @@ -118,7 +123,10 @@ export const playlistFields = [ { displayName: 'Default Language', name: 'defaultLanguage', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, default: '', description: `The language of the text in the playlist resource's title and description properties.`, }, @@ -141,6 +149,104 @@ export const playlistFields = [ ], }, /* -------------------------------------------------------------------------- */ + /* playlist:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Playlist ID', + name: 'playlistId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'playlist', + ], + }, + }, + default: '', + }, + { + displayName: 'Fields', + name: 'part', + type: 'multiOptions', + options: [ + { + name: 'Content Details', + value: 'contentDetails', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Localizations', + value: 'localizations', + }, + { + name: 'Player', + value: 'player', + }, + { + name: 'Snippet', + value: 'snippet', + }, + { + name: 'Status', + value: 'status', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'playlist', + ], + }, + }, + description: 'The fields parameter specifies a comma-separated list of one or more playlist resource properties that the API response will include.', + default: '' + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'playlist', + ], + }, + }, + options: [ + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + { + displayName: 'On Behalf Of Content Owner Channel', + name: 'onBehalfOfContentOwnerChannel', + type: 'string', + default: '', + description: `The onBehalfOfContentOwnerChannel parameter specifies the YouTube channel ID of the channel to which a video is being added`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ /* playlist:delete */ /* -------------------------------------------------------------------------- */ { @@ -306,13 +412,6 @@ export const playlistFields = [ default: '', description: `The id parameter specifies a comma-separated list of the YouTube playlist ID(s) for the resource(s) that are being retrieved. In a playlist resource, the id property specifies the playlist's YouTube playlist ID.`, }, - { - displayName: 'Mine', - name: 'mine', - type: 'boolean', - default: false, - description: `Set this parameter's value to true to instruct the API to only return playlists owned by the authenticated user.`, - }, ], }, { @@ -371,6 +470,24 @@ export const playlistFields = [ default: '', description: `The playlist's title.`, }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'playlist', + ], + }, + }, + default: '', + description: `The playlist's title.`, + }, { displayName: 'Update Fields', name: 'updateFields', @@ -388,6 +505,16 @@ export const playlistFields = [ }, }, options: [ + { + displayName: 'Default Language', + name: 'defaultLanguage', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, + default: '', + description: `The language of the text in the playlist resource's title and description properties.`, + }, { displayName: 'Description', name: 'description', @@ -395,6 +522,14 @@ export const playlistFields = [ default: '', description: `The playlist's description.`, }, + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, { displayName: 'Privacy Status', name: 'privacyStatus', @@ -423,27 +558,6 @@ export const playlistFields = [ default: '', description: `Keyword tags associated with the playlist. Mulplie can be defined separated by comma`, }, - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - }, - { - displayName: 'Default Language', - name: 'defaultLanguage', - type: 'string', - default: '', - description: `The language of the text in the playlist resource's title and description properties.`, - }, - { - displayName: 'On Behalf Of Content Owner', - name: 'onBehalfOfContentOwner', - type: 'string', - default: '', - description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
- a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, - }, ], }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/YouTube/VideoCategoryDescription.ts b/packages/nodes-base/nodes/Google/YouTube/VideoCategoryDescription.ts new file mode 100644 index 0000000000..fcb308e1a7 --- /dev/null +++ b/packages/nodes-base/nodes/Google/YouTube/VideoCategoryDescription.ts @@ -0,0 +1,95 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const videoCategoryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'videoCategory', + ], + }, + }, + options: [ + + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all video categories', + }, + ], + default: 'getAll', + description: 'The operation to perform.' + } +] as INodeProperties[]; + +export const videoCategoryFields = [ + /* -------------------------------------------------------------------------- */ + /* videoCategory:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Region Code', + name: 'regionCode', + type: 'options', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'videoCategory', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getCountriesCodes', + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'videoCategory', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'videoCategory', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts index b7c4f9d294..8c9b9ea80b 100644 --- a/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts @@ -73,8 +73,8 @@ export const videoFields = [ default: '', }, { - displayName: 'Country Code', - name: 'countryCode', + displayName: 'Region Code', + name: 'regionCode', type: 'options', typeOptions: { loadOptionsMethod: 'getCountriesCodes', @@ -98,7 +98,7 @@ export const videoFields = [ typeOptions: { loadOptionsMethod: 'getVideoCategories', loadOptionsDependsOn: [ - 'countryCode', + 'regionCode', ], }, displayOptions: { @@ -150,7 +150,10 @@ export const videoFields = [ { displayName: 'Default Language', name: 'defaultLanguage', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, default: '', description: `The language of the text in the playlist resource's title and description properties.`, }, @@ -182,7 +185,7 @@ export const videoFields = [ value: 'youtube', }, ], - default: false, + default: '', description: `The video's license.`, }, { @@ -326,10 +329,6 @@ export const videoFields = [ name: 'Content Details', value: 'contentDetails', }, - { - name: 'Field Details', - value: 'fieldDetails', - }, { name: 'ID', value: 'id', @@ -346,10 +345,6 @@ export const videoFields = [ name: 'Player', value: 'player', }, - { - name: 'Processing Details', - value: 'processingDetails', - }, { name: 'Recording Details', value: 'recordingDetails', @@ -428,10 +423,6 @@ export const videoFields = [ name: 'Content Details', value: 'contentDetails', }, - { - name: 'File Details', - value: 'fileDetails', - }, { name: 'ID', value: 'id', @@ -448,10 +439,6 @@ export const videoFields = [ name: 'Player', value: 'player', }, - { - name: 'Processing Details', - value: 'processingDetails', - }, { name: 'Recording Details', value: 'recordingDetails', @@ -692,47 +679,64 @@ export const videoFields = [ }, default: '', }, - // { - // displayName: 'Country Code', - // name: 'countryCode', - // type: 'options', - // typeOptions: { - // loadOptionsMethod: 'getCountriesCodes', - // }, - // displayOptions: { - // show: { - // operation: [ - // 'update', - // ], - // resource: [ - // 'video', - // ], - // }, - // }, - // default: '', - // }, - // { - // displayName: 'Category ID', - // name: 'categoryId', - // type: 'options', - // typeOptions: { - // loadOptionsMethod: 'getVideoCategories', - // loadOptionsDependsOn: [ - // 'countryCode', - // ], - // }, - // displayOptions: { - // show: { - // operation: [ - // 'update', - // ], - // resource: [ - // 'video', - // ], - // }, - // }, - // default: '', - // }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'video', + ], + }, + }, + default: '', + }, + { + displayName: 'Region Code', + name: 'regionCode', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCountriesCodes', + }, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'video', + ], + }, + }, + default: '', + }, + { + displayName: 'Category ID', + name: 'categoryId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getVideoCategories', + loadOptionsDependsOn: [ + 'regionCode', + ], + }, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'video', + ], + }, + }, + default: '', + }, { displayName: 'Update Fields', name: 'updateFields', @@ -753,7 +757,10 @@ export const videoFields = [ { displayName: 'Default Language', name: 'defaultLanguage', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, default: '', description: `The language of the text in the playlist resource's title and description properties.`, }, @@ -785,7 +792,7 @@ export const videoFields = [ value: 'youtube', }, ], - default: false, + default: '', description: `The video's license.`, }, { @@ -851,12 +858,6 @@ export const videoFields = [ default: '', description: `Keyword tags associated with the playlist. Mulplie can be defined separated by comma`, }, - { - displayName: 'Title', - name: 'title', - type: 'string', - default: '', - }, ], }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts index fe9c8d5309..0c09c391dd 100644 --- a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts +++ b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts @@ -32,6 +32,11 @@ import { videoFields, } from './VideoDescription'; +import { + videoCategoryOperations, + videoCategoryFields, +} from './VideoCategoryDescription'; + import { countriesCodes, } from './CountryCodes'; @@ -75,6 +80,10 @@ export class YouTube implements INodeType { name: 'Video', value: 'video', }, + { + name: 'Video Category', + value: 'videoCategory', + }, ], default: 'channel', description: 'The resource to operate on.' @@ -87,6 +96,9 @@ export class YouTube implements INodeType { ...videoOperations, ...videoFields, + + ...videoCategoryOperations, + ...videoCategoryFields, ], }; @@ -133,7 +145,8 @@ export class YouTube implements INodeType { async getVideoCategories( this: ILoadOptionsFunctions ): Promise { - const countryCode = this.getCurrentNodeParameter('countryCode') as string; + const countryCode = this.getCurrentNodeParameter('regionCode') as string; + const returnData: INodePropertyOptions[] = []; const qs: IDataObject = {}; qs.regionCode = countryCode; @@ -169,6 +182,25 @@ export class YouTube implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { if (resource === 'channel') { + if (operation === 'get') { + //https://developers.google.com/youtube/v3/docs/channels/list + const part = this.getNodeParameter('part', i) as string[]; + const channelId = this.getNodeParameter('channelId', i) as string; + + qs.part = part.join(','); + + qs.id = channelId; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/youtube/v3/channels`, + {}, + qs + ); + + responseData = responseData.items; + } //https://developers.google.com/youtube/v3/docs/channels/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; @@ -178,27 +210,14 @@ export class YouTube implements INodeType { qs.part = part.join(','); - if (filters.categoryId) { - qs.categoryId = filters.categoryId as string; - } - if (filters.forUsername) { - qs.forUsername = filters.forUsername as string; - } - if (filters.id) { - qs.id = filters.id as string; - } - if (filters.managedByMe) { - qs.managedByMe = filters.managedByMe as boolean; - } - if (filters.mine) { - qs.mine = filters.mine as boolean; - } - if (options.h1) { - qs.h1 = options.h1 as string; - } - if (options.onBehalfOfContentOwner) { - qs.onBehalfOfContentOwner = options.onBehalfOfContentOwner as string; + Object.assign(qs, options, filters); + + qs.mine = true; + + if (qs.categoryId || qs.forUsername || qs.id || qs.managedByMe) { + delete qs.mine; } + if (returnAll) { responseData = await googleApiRequestAllItems.call( this, @@ -371,6 +390,28 @@ export class YouTube implements INodeType { } } if (resource === 'playlist') { + //https://developers.google.com/youtube/v3/docs/playlists/list + if (operation === 'get') { + const part = this.getNodeParameter('part', i) as string[]; + const playlistId = this.getNodeParameter('playlistId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + qs.part = part.join(','); + + qs.id = playlistId; + + Object.assign(qs, options); + + responseData = await googleApiRequest.call( + this, + 'GET', + `/youtube/v3/playlists`, + {}, + qs + ); + + responseData = responseData.items; + } //https://developers.google.com/youtube/v3/docs/playlists/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; @@ -382,6 +423,12 @@ export class YouTube implements INodeType { Object.assign(qs, options, filters); + qs.mine = true; + + if (qs.channelId || qs.id) { + delete qs.mine; + } + if (returnAll) { responseData = await googleApiRequestAllItems.call( this, @@ -450,14 +497,18 @@ export class YouTube implements INodeType { //https://developers.google.com/youtube/v3/docs/playlists/update if (operation === 'update') { const playlistId = this.getNodeParameter('playlistId', i) as string; + const title = this.getNodeParameter('title', i) as string; const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; - qs.part = 'snippet'; + qs.part = 'snippet, status'; const body: IDataObject = { id: playlistId, snippet: { - } + title, + }, + status: { + }, }; if (updateFields.tags) { @@ -465,9 +516,9 @@ export class YouTube implements INodeType { body.snippet.tags = (updateFields.tags as string).split(',') as string[]; } - if (updateFields.title) { + if (updateFields.privacyStatus) { //@ts-ignore - body.snippet.title = updateFields.title as string + body.status.privacyStatus = updateFields.privacyStatus as string; } if (updateFields.description) { @@ -529,6 +580,10 @@ export class YouTube implements INodeType { Object.assign(qs, options, filters); + if (qs.myRating) { + delete qs.chart; + } + if (returnAll) { responseData = await googleApiRequestAllItems.call( this, @@ -689,7 +744,8 @@ export class YouTube implements INodeType { //https://developers.google.com/youtube/v3/docs/playlists/update if (operation === 'update') { const id = this.getNodeParameter('videoId', i) as string; - //const categoryId = this.getNodeParameter('categoryId', i) as string; + const title = this.getNodeParameter('title', i) as string; + const categoryId = this.getNodeParameter('categoryId', i) as string; const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; qs.part = 'snippet, status, recordingDetails'; @@ -697,7 +753,8 @@ export class YouTube implements INodeType { const body = { id, snippet: { - //categoryId, + title, + categoryId, }, status: { }, @@ -707,59 +764,56 @@ export class YouTube implements INodeType { if (updateFields.description) { //@ts-ignore - data.snippet.description = updateFields.description as string; + body.snippet.description = updateFields.description as string; } if (updateFields.privacyStatus) { //@ts-ignore - data.status.privacyStatus = updateFields.privacyStatus as string; + body.status.privacyStatus = updateFields.privacyStatus as string; } if (updateFields.tags) { //@ts-ignore - data.snippet.tags = (updateFields.tags as string).split(',') as string[]; - } - - if (updateFields.description) { - //@ts-ignore - data.snippet.title = updateFields.title as string; + body.snippet.tags = (updateFields.tags as string).split(',') as string[]; } if (updateFields.embeddable) { //@ts-ignore - data.status.embeddable = updateFields.embeddable as boolean; + body.status.embeddable = updateFields.embeddable as boolean; } if (updateFields.publicStatsViewable) { //@ts-ignore - data.status.publicStatsViewable = updateFields.publicStatsViewable as boolean; + body.status.publicStatsViewable = updateFields.publicStatsViewable as boolean; } if (updateFields.publishAt) { //@ts-ignore - data.status.publishAt = updateFields.publishAt as string; - } - - if (updateFields.recordingDate) { - //@ts-ignore - data.recordingDetails.recordingDate = updateFields.recordingDate as string; + body.status.publishAt = updateFields.publishAt as string; } if (updateFields.selfDeclaredMadeForKids) { //@ts-ignore - data.status.selfDeclaredMadeForKids = updateFields.selfDeclaredMadeForKids as boolean; + body.status.selfDeclaredMadeForKids = updateFields.selfDeclaredMadeForKids as boolean; + } + + if (updateFields.recordingDate) { + //@ts-ignore + body.recordingDetails.recordingDate = updateFields.recordingDate as string; } if (updateFields.license) { //@ts-ignore - data.status.license = options.license as string; + body.status.license = updateFields.license as string; } if (updateFields.defaultLanguage) { //@ts-ignore - data.snippet.defaultLanguage = updateFields.defaultLanguage as string; + body.snippet.defaultLanguage = updateFields.defaultLanguage as string; } + console.log(body) + responseData = await googleApiRequest.call( this, 'PUT', @@ -810,6 +864,31 @@ export class YouTube implements INodeType { responseData = { success: true }; } } + if (resource === 'videoCategory') { + //https://developers.google.com/youtube/v3/docs/videoCategories/list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const regionCode = this.getNodeParameter('regionCode', i) as string; + + qs.regionCode = regionCode; + + qs.part = 'snippet'; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/youtube/v3/videoCategories`, + {}, + qs + ); + responseData = responseData.items; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); From 091659df29eb3c9aaadd9b77a75b2fe558f8b262 Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 14 Aug 2020 18:36:47 -0400 Subject: [PATCH 5/6] zap: Improvements --- .../Google/YouTube/ChannelDescription.ts | 14 +- .../Google/YouTube/PlaylistDescription.ts | 12 +- .../YouTube/VideoCategoryDescription.ts | 4 +- .../nodes/Google/YouTube/VideoDescription.ts | 199 ++++++++++-------- .../nodes/Google/YouTube/YouTube.node.ts | 100 +++++++-- 5 files changed, 215 insertions(+), 114 deletions(-) diff --git a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts index 5d08941aeb..6a45d7ec69 100644 --- a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts @@ -50,6 +50,10 @@ export const channelFields = [ name: 'part', type: 'multiOptions', options: [ + { + name: '*', + value: '*', + }, { name: 'Branding Settings', value: 'brandingSettings', @@ -137,9 +141,9 @@ export const channelFields = [ }, typeOptions: { minValue: 1, - maxValue: 500, + maxValue: 50, }, - default: 100, + default: 25, description: 'How many results to return.', }, { @@ -243,6 +247,7 @@ export const channelFields = [ ], }, }, + description: 'ID of the video', default: '', }, { @@ -250,6 +255,10 @@ export const channelFields = [ name: 'part', type: 'multiOptions', options: [ + { + name: '*', + value: '*', + }, { name: 'Branding Settings', value: 'brandingSettings', @@ -554,6 +563,7 @@ export const channelFields = [ ], }, }, + description: 'ID of the channel', default: '', }, { diff --git a/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts b/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts index 5cb58ee97b..8383ec6ef1 100644 --- a/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts @@ -173,6 +173,10 @@ export const playlistFields = [ name: 'part', type: 'multiOptions', options: [ + { + name: '*', + value: '*', + }, { name: 'Content Details', value: 'contentDetails', @@ -301,6 +305,10 @@ export const playlistFields = [ name: 'part', type: 'multiOptions', options: [ + { + name: '*', + value: '*', + }, { name: 'Content Details', value: 'contentDetails', @@ -376,9 +384,9 @@ export const playlistFields = [ }, typeOptions: { minValue: 1, - maxValue: 500, + maxValue: 50, }, - default: 100, + default: 25, description: 'How many results to return.', }, { diff --git a/packages/nodes-base/nodes/Google/YouTube/VideoCategoryDescription.ts b/packages/nodes-base/nodes/Google/YouTube/VideoCategoryDescription.ts index fcb308e1a7..1f52ff23d9 100644 --- a/packages/nodes-base/nodes/Google/YouTube/VideoCategoryDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/VideoCategoryDescription.ts @@ -87,9 +87,9 @@ export const videoCategoryFields = [ }, typeOptions: { minValue: 1, - maxValue: 500, + maxValue: 50, }, - default: 100, + default: 25, description: 'How many results to return.', }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts index 8c9b9ea80b..aee1d0bc66 100644 --- a/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts @@ -1,6 +1,7 @@ import { INodeProperties, } from 'n8n-workflow'; +import { id } from 'rhea'; export const videoOperations = [ { @@ -271,6 +272,7 @@ export const videoFields = [ ], }, }, + description: 'ID of the video', default: '', }, { @@ -325,6 +327,10 @@ export const videoFields = [ name: 'part', type: 'multiOptions', options: [ + { + name: '*', + value: '*', + }, { name: 'Content Details', value: 'contentDetails', @@ -361,10 +367,6 @@ export const videoFields = [ name: 'Status', value: 'status', }, - { - name: 'Suggestions', - value: 'suggestions', - }, { name: 'Topic Details', value: 'topicDetails', @@ -414,70 +416,6 @@ export const videoFields = [ /* -------------------------------------------------------------------------- */ /* video:getAll */ /* -------------------------------------------------------------------------- */ - { - displayName: 'Fields', - name: 'part', - type: 'multiOptions', - options: [ - { - name: 'Content Details', - value: 'contentDetails', - }, - { - name: 'ID', - value: 'id', - }, - { - name: 'Live Streaming Details', - value: 'liveStreamingDetails', - }, - { - name: 'Localizations', - value: 'localizations', - }, - { - name: 'Player', - value: 'player', - }, - { - name: 'Recording Details', - value: 'recordingDetails', - }, - { - name: 'Snippet', - value: 'snippet', - }, - { - name: 'Statistics', - value: 'statistics', - }, - { - name: 'Status', - value: 'status', - }, - { - name: 'Suggestions', - value: 'suggestions', - }, - { - name: 'Topic Details', - value: 'topicDetails', - }, - ], - required: true, - displayOptions: { - show: { - operation: [ - 'getAll', - ], - resource: [ - 'video', - ], - }, - }, - description: 'The fields parameter specifies a comma-separated list of one or more video resource properties that the API response will include.', - default: '' - }, { displayName: 'Return All', name: 'returnAll', @@ -514,9 +452,9 @@ export const videoFields = [ }, typeOptions: { minValue: 1, - maxValue: 500, + maxValue: 50, }, - default: 100, + default: 25, description: 'How many results to return.', }, { @@ -537,28 +475,39 @@ export const videoFields = [ }, options: [ { - displayName: 'ID', - name: 'id', + displayName: 'Channel ID', + name: 'channelId', type: 'string', default: '', - description: `The id parameter specifies a comma-separated list of the YouTube video ID(s) for the resource(s) that are being retrieved. In a video resource, the id property specifies the video's YouTube video ID.`, + description: `The channelId parameter indicates that the API response should only contain resources created by the channel.`, }, { - displayName: 'My Rating', - name: 'myRating', - type: 'options', - options: [ - { - name: 'Dislike', - value: 'dislike', - }, - { - name: 'Like', - value: 'like', - }, - ], + displayName: 'For Developer', + name: 'forDeveloper', + type: 'boolean', + default: false, + description: `The forDeveloper parameter restricts the search to only retrieve videos uploaded via the developer's application or website`, + }, + { + displayName: 'Published After', + name: 'publishedAfter', + type: 'dateTime', default: '', - description: `Set this parameter's value to like or dislike to instruct the API to only return videos liked or disliked by the authenticated user.`, + description: `The publishedAfter parameter indicates that the API response should only contain resources created at or after the specified time.`, + }, + { + displayName: 'Published Before', + name: 'publishedBefore', + type: 'dateTime', + default: '', + description: `The publishedBefore parameter indicates that the API response should only contain resources created before or at the specified time.`, + }, + { + displayName: 'Query', + name: 'q', + type: 'string', + default: '', + description: `The q parameter specifies the query term to search for.`, }, { displayName: 'Region Code', @@ -570,6 +519,13 @@ export const videoFields = [ default: '', description: `The regionCode parameter instructs the API to select a video chart available in the specified region.`, }, + { + displayName: 'Related To Video ID', + name: 'relatedToVideoId', + type: 'string', + default: '', + description: 'The relatedToVideoId parameter retrieves a list of videos that are related to the video that the parameter value identifies', + }, { displayName: 'Video Category ID', name: 'videoCategoryId', @@ -577,6 +533,34 @@ export const videoFields = [ default: '', description: `The videoCategoryId parameter identifies the video category for which the chart should be retrieved.`, }, + { + displayName: 'Video Syndicated ', + name: 'videoSyndicated', + type: 'boolean', + default: false, + description: `The videoSyndicated parameter lets you to restrict a search to only videos that can be played outside youtube.com.`, + }, + { + displayName: 'Video Type', + name: 'videoType', + type: 'options', + options: [ + { + name: 'Any', + value: 'any', + }, + { + name: 'Episode', + value: 'episode', + }, + { + name: 'Movie', + value: 'movie', + }, + ], + default: '', + description: `The videoType parameter lets you restrict a search to a particular type of videos`, + }, ], }, { @@ -597,12 +581,43 @@ export const videoFields = [ }, options: [ { - displayName: 'On Behalf Of Content Owner', - name: 'onBehalfOfContentOwner', - type: 'string', + displayName: 'Order', + name: 'order', + type: 'options', + options: [ + { + name: 'Date', + value: 'date', + }, + { + name: 'Relevance', + value: 'relevance', + }, + ], + default: 'relevance', + }, + { + displayName: 'Safe Search', + name: 'safeSearch', + type: 'options', + options: [ + { + name: 'Moderate', + value: 'moderate', + description: 'YouTube will filter some content from search results and, at the least, will filter content that is restricted in your locale', + }, + { + name: 'none', + value: 'none', + description: 'YouTube will not filter the search result set', + }, + { + name: 'Strict', + value: 'strict', + description: 'YouTube will try to exclude all restricted content from the search result set', + }, + ], default: '', - description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
- a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, }, ], }, diff --git a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts index 0c09c391dd..e838bdad46 100644 --- a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts +++ b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts @@ -184,9 +184,23 @@ export class YouTube implements INodeType { if (resource === 'channel') { if (operation === 'get') { //https://developers.google.com/youtube/v3/docs/channels/list - const part = this.getNodeParameter('part', i) as string[]; + let part = this.getNodeParameter('part', i) as string[]; const channelId = this.getNodeParameter('channelId', i) as string; + if (part.includes('*')) { + part = [ + 'brandingSettings', + 'contentDetails', + 'contentOwnerDetails', + 'id', + 'localizations', + 'snippet', + 'statistics', + 'status', + 'topicDetails', + ]; + } + qs.part = part.join(','); qs.id = channelId; @@ -204,10 +218,24 @@ export class YouTube implements INodeType { //https://developers.google.com/youtube/v3/docs/channels/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; - const part = this.getNodeParameter('part', i) as string[]; + let part = this.getNodeParameter('part', i) as string[]; const options = this.getNodeParameter('options', i) as IDataObject; const filters = this.getNodeParameter('filters', i) as IDataObject; + if (part.includes('*')) { + part = [ + 'brandingSettings', + 'contentDetails', + 'contentOwnerDetails', + 'id', + 'localizations', + 'snippet', + 'statistics', + 'status', + 'topicDetails', + ]; + } + qs.part = part.join(','); Object.assign(qs, options, filters); @@ -392,10 +420,21 @@ export class YouTube implements INodeType { if (resource === 'playlist') { //https://developers.google.com/youtube/v3/docs/playlists/list if (operation === 'get') { - const part = this.getNodeParameter('part', i) as string[]; + let part = this.getNodeParameter('part', i) as string[]; const playlistId = this.getNodeParameter('playlistId', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; + if (part.includes('*')) { + part = [ + 'contentDetails', + 'id', + 'localizations', + 'player', + 'snippet', + 'status', + ]; + } + qs.part = part.join(','); qs.id = playlistId; @@ -415,10 +454,21 @@ export class YouTube implements INodeType { //https://developers.google.com/youtube/v3/docs/playlists/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; - const part = this.getNodeParameter('part', i) as string[]; + let part = this.getNodeParameter('part', i) as string[]; const options = this.getNodeParameter('options', i) as IDataObject; const filters = this.getNodeParameter('filters', i) as IDataObject; + if (part.includes('*')) { + part = [ + 'contentDetails', + 'id', + 'localizations', + 'player', + 'snippet', + 'status', + ]; + } + qs.part = part.join(','); Object.assign(qs, options, filters); @@ -567,21 +617,26 @@ export class YouTube implements INodeType { } } if (resource === 'video') { - //https://developers.google.com/youtube/v3/docs/videos/list?hl=en + //https://developers.google.com/youtube/v3/docs/search/list if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; - const part = this.getNodeParameter('part', i) as string[]; const options = this.getNodeParameter('options', i) as IDataObject; const filters = this.getNodeParameter('filters', i) as IDataObject; - qs.part = part.join(','); + qs.part = 'snippet'; - qs.chart = 'mostPopular'; + qs.type = 'video'; + + qs.forMine = true; Object.assign(qs, options, filters); - if (qs.myRating) { - delete qs.chart; + if (Object.keys(filters).length > 0) { + delete qs.forMine; + } + + if (qs.relatedToVideoId && qs.forDeveloper !== undefined) { + throw new Error(`When using the parameter 'related to video' the parameter 'for developer' cannot be set`); } if (returnAll) { @@ -589,7 +644,7 @@ export class YouTube implements INodeType { this, 'items', 'GET', - `/youtube/v3/videos`, + `/youtube/v3/search`, {}, qs ); @@ -598,7 +653,7 @@ export class YouTube implements INodeType { responseData = await googleApiRequest.call( this, 'GET', - `/youtube/v3/videos`, + `/youtube/v3/search`, {}, qs ); @@ -607,10 +662,25 @@ export class YouTube implements INodeType { } //https://developers.google.com/youtube/v3/docs/videos/list?hl=en if (operation === 'get') { - const part = this.getNodeParameter('part', i) as string[]; + let part = this.getNodeParameter('part', i) as string[]; const videoId = this.getNodeParameter('videoId', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; + if (part.includes('*')) { + part = [ + 'contentDetails', + 'id', + 'liveStreamingDetails', + 'localizations', + 'player', + 'recordingDetails', + 'snippet', + 'statistics', + 'status', + 'topicDetails', + ]; + } + qs.part = part.join(','); qs.id = videoId; @@ -812,8 +882,6 @@ export class YouTube implements INodeType { body.snippet.defaultLanguage = updateFields.defaultLanguage as string; } - console.log(body) - responseData = await googleApiRequest.call( this, 'PUT', @@ -838,7 +906,7 @@ export class YouTube implements INodeType { responseData = await googleApiRequest.call( this, 'DELETE', - '/youtube/v3/video', + '/youtube/v3/videos', body, ); From 54f103776e83ee25907bbb6cec98a7313e64dc18 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sat, 15 Aug 2020 22:36:11 -0400 Subject: [PATCH 6/6] :zap: add playlist-item resource --- .../Google/YouTube/ChannelDescription.ts | 4 +- .../Google/YouTube/PlaylistDescription.ts | 4 +- .../Google/YouTube/PlaylistItemDescription.ts | 408 ++++++++++++++++++ .../nodes/Google/YouTube/VideoDescription.ts | 3 +- .../nodes/Google/YouTube/YouTube.node.ts | 193 ++++++++- 5 files changed, 605 insertions(+), 7 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/YouTube/PlaylistItemDescription.ts diff --git a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts index 6a45d7ec69..4c6a5188f7 100644 --- a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts @@ -103,7 +103,7 @@ export const channelFields = [ }, }, description: 'The fields parameter specifies a comma-separated list of one or more channel resource properties that the API response will include.', - default: '' + default: ['*'], }, { displayName: 'Return All', @@ -308,7 +308,7 @@ export const channelFields = [ }, }, description: 'The fields parameter specifies a comma-separated list of one or more channel resource properties that the API response will include.', - default: '' + default: ['*'], }, /* -------------------------------------------------------------------------- */ /* channel:update */ diff --git a/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts b/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts index 8383ec6ef1..cb6508e458 100644 --- a/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/PlaylistDescription.ts @@ -214,7 +214,7 @@ export const playlistFields = [ }, }, description: 'The fields parameter specifies a comma-separated list of one or more playlist resource properties that the API response will include.', - default: '' + default: ['*'], }, { displayName: 'Options', @@ -346,7 +346,7 @@ export const playlistFields = [ }, }, description: 'The fields parameter specifies a comma-separated list of one or more playlist resource properties that the API response will include.', - default: '' + default: ['*'], }, { displayName: 'Return All', diff --git a/packages/nodes-base/nodes/Google/YouTube/PlaylistItemDescription.ts b/packages/nodes-base/nodes/Google/YouTube/PlaylistItemDescription.ts new file mode 100644 index 0000000000..88efa29acc --- /dev/null +++ b/packages/nodes-base/nodes/Google/YouTube/PlaylistItemDescription.ts @@ -0,0 +1,408 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const playlistItemOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'playlistItem', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add an item to a playlist', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a item from a playlist', + }, + { + name: 'Get', + value: 'get', + description: `Get a playlist's item`, + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all playlist items', + }, + ], + default: 'add', + description: 'The operation to perform.' + } +] as INodeProperties[]; + +export const playlistItemFields = [ + /* -------------------------------------------------------------------------- */ + /* playlistItem:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Playlist ID', + name: 'playlistId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPlaylists', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'playlistItem', + ], + }, + }, + default: '', + }, + { + displayName: 'Video ID', + name: 'videoId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'playlistItem', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'playlistItem', + ], + }, + }, + options: [ + { + displayName: 'Position', + name: 'position', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: '', + description: `The order in which the item appears in the playlist. The value uses a zero-based index, so the first item has a position of 0, the second item has a position of 1, and so forth.`, + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + description: `A user-generated note for this item. The property value has a maximum length of 280 characters.`, + }, + { + displayName: 'Start At', + name: 'startAt', + type: 'dateTime', + default: '', + description: `The time, measured in seconds from the start of the video, when the video should start playing.`, + }, + { + displayName: 'End At', + name: 'endAt', + type: 'dateTime', + default: '', + description: `The time, measured in seconds from the start of the video, when the video should stop playing.`, + }, + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* playlistItem:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Playlist Item ID', + name: 'playlistItemId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'playlistItem', + ], + }, + }, + default: '', + }, + { + displayName: 'Fields', + name: 'part', + type: 'multiOptions', + options: [ + { + name: '*', + value: '*', + }, + { + name: 'Content Details', + value: 'contentDetails', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Snippet', + value: 'snippet', + }, + { + name: 'Status', + value: 'status', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'playlistItem', + ], + }, + }, + description: 'The fields parameter specifies a comma-separated list of one or more playlistItem resource properties that the API response will include.', + default: ['*'], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'playlistItem', + ], + }, + }, + options: [ + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* playlistItem:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Playlist Item ID', + name: 'playlistItemId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'playlistItem', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'playlistItem', + ], + }, + }, + options: [ + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* playlistItem:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Playlist ID', + name: 'playlistId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPlaylists', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlistItem', + ], + }, + }, + default: '' + }, + { + displayName: 'Fields', + name: 'part', + type: 'multiOptions', + options: [ + { + name: '*', + value: '*', + }, + { + name: 'Content Details', + value: 'contentDetails', + }, + { + name: 'ID', + value: 'id', + }, + { + name: 'Snippet', + value: 'snippet', + }, + { + name: 'Status', + value: 'status', + }, + ], + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlistItem', + ], + }, + }, + description: 'The fields parameter specifies a comma-separated list of one or more playlistItem resource properties that the API response will include.', + default: ['*'], + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlistItem', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlistItem', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 50, + }, + default: 25, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'playlistItem', + ], + }, + }, + options: [ + { + displayName: 'On Behalf Of Content Owner', + name: 'onBehalfOfContentOwner', + type: 'string', + default: '', + description: `The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify
+ a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts index aee1d0bc66..1f5f90902e 100644 --- a/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts @@ -1,7 +1,6 @@ import { INodeProperties, } from 'n8n-workflow'; -import { id } from 'rhea'; export const videoOperations = [ { @@ -384,7 +383,7 @@ export const videoFields = [ }, }, description: 'The fields parameter specifies a comma-separated list of one or more video resource properties that the API response will include.', - default: '' + default: ['*'], }, { displayName: 'Options', diff --git a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts index e838bdad46..f64d4aeb11 100644 --- a/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts +++ b/packages/nodes-base/nodes/Google/YouTube/YouTube.node.ts @@ -27,6 +27,11 @@ import { playlistFields, } from './PlaylistDescription'; +import { + playlistItemOperations, + playlistItemFields, +} from './PlaylistItemDescription'; + import { videoOperations, videoFields, @@ -60,7 +65,7 @@ export class YouTube implements INodeType { { name: 'youTubeOAuth2Api', required: true, - } + }, ], properties: [ { @@ -76,6 +81,10 @@ export class YouTube implements INodeType { name: 'Playlist', value: 'playlist', }, + { + name: 'Playlist Item', + value: 'playlistItem', + }, { name: 'Video', value: 'video', @@ -94,6 +103,9 @@ export class YouTube implements INodeType { ...playlistOperations, ...playlistFields, + ...playlistItemOperations, + ...playlistItemFields, + ...videoOperations, ...videoFields, @@ -169,6 +181,33 @@ export class YouTube implements INodeType { } return returnData; }, + // Get all the playlists to display them to user so that he can + // select them easily + async getPlaylists( + this: ILoadOptionsFunctions + ): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = {}; + qs.part = 'snippet'; + qs.mine = true; + const playlists = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + '/youtube/v3/playlists', + {}, + qs, + ); + for (const playlist of playlists) { + const playlistName = playlist.snippet.title; + const playlistId = playlist.id; + returnData.push({ + name: playlistName, + value: playlistId + }); + } + return returnData; + }, } }; @@ -616,6 +655,158 @@ export class YouTube implements INodeType { responseData = { success: true }; } } + if (resource === 'playlistItem') { + //https://developers.google.com/youtube/v3/docs/playlistItems/list + if (operation === 'get') { + let part = this.getNodeParameter('part', i) as string[]; + const playlistItemId = this.getNodeParameter('playlistItemId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + if (part.includes('*')) { + part = [ + 'contentDetails', + 'id', + 'snippet', + 'status', + ]; + } + + qs.part = part.join(','); + + qs.id = playlistItemId; + + Object.assign(qs, options); + + responseData = await googleApiRequest.call( + this, + 'GET', + `/youtube/v3/playlistItems`, + {}, + qs + ); + + responseData = responseData.items; + } + //https://developers.google.com/youtube/v3/docs/playlistItems/list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + let part = this.getNodeParameter('part', i) as string[]; + const options = this.getNodeParameter('options', i) as IDataObject; + const playlistId = this.getNodeParameter('playlistId', i) as string; + //const filters = this.getNodeParameter('filters', i) as IDataObject; + + if (part.includes('*')) { + part = [ + 'contentDetails', + 'id', + 'snippet', + 'status', + ]; + } + + qs.playlistId = playlistId; + + qs.part = part.join(','); + + Object.assign(qs, options); + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + `/youtube/v3/playlistItems`, + {}, + qs + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/youtube/v3/playlistItems`, + {}, + qs + ); + responseData = responseData.items; + } + } + //https://developers.google.com/youtube/v3/docs/playlistItems/insert + if (operation === 'add') { + const playlistId = this.getNodeParameter('playlistId', i) as string; + const videoId = this.getNodeParameter('videoId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + qs.part = 'snippet, contentDetails'; + + const body: IDataObject = { + snippet: { + playlistId, + resourceId: { + kind: 'youtube#video', + videoId: videoId, + }, + }, + contentDetails: { + }, + + }; + + if (options.position) { + //@ts-ignore + body.snippet.position = options.position as number; + } + + if (options.note) { + //@ts-ignore + body.contentDetails.note = options.note as string; + } + + if (options.startAt) { + //@ts-ignore + body.contentDetails.startAt = options.startAt as string; + } + + if (options.endAt) { + //@ts-ignore + body.contentDetails.endAt = options.endAt as string; + } + + if (options.onBehalfOfContentOwner) { + qs.onBehalfOfContentOwner = options.onBehalfOfContentOwner as string; + } + + responseData = await googleApiRequest.call( + this, + 'POST', + '/youtube/v3/playlistItems', + body, + qs + ); + } + //https://developers.google.com/youtube/v3/docs/playlistItems/delete + if (operation === 'delete') { + const playlistItemId = this.getNodeParameter('playlistItemId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + const body: IDataObject = { + id: playlistItemId, + }; + + if (options.onBehalfOfContentOwner) { + qs.onBehalfOfContentOwner = options.onBehalfOfContentOwner as string; + } + + responseData = await googleApiRequest.call( + this, + 'DELETE', + '/youtube/v3/playlistItems', + body, + ); + + responseData = { success: true }; + } + } if (resource === 'video') { //https://developers.google.com/youtube/v3/docs/search/list if (operation === 'getAll') {