diff --git a/components/youtube_analytics_api/actions/common/reports-query.mjs b/components/youtube_analytics_api/actions/common/reports-query.mjs new file mode 100644 index 0000000000000..542f55179389c --- /dev/null +++ b/components/youtube_analytics_api/actions/common/reports-query.mjs @@ -0,0 +1,119 @@ +import app from "../../youtube_analytics_api.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; + +export default { + props: { + app, + reloader: { + type: "boolean", + label: "Hidden Reloader", + description: "This prop is used to reload the props when the step gets created.", + hidden: true, + reloadProps: true, + }, + startDate: { + propDefinition: [ + app, + "startDate", + ], + }, + endDate: { + propDefinition: [ + app, + "endDate", + ], + }, + dimensions: { + propDefinition: [ + app, + "dimensions", + ], + }, + sort: { + propDefinition: [ + app, + "sort", + ], + }, + maxResults: { + propDefinition: [ + app, + "maxResults", + ], + }, + }, + methods: { + getIdsProps() { + const { idType } = this; + + if (idType === constants.ID_TYPE.CHANNEL.value) { + return { + idType: propsFragments.idType, + }; + } + + if (idType === constants.ID_TYPE.CONTENT_OWNER.value) { + return { + idType: propsFragments.idType, + ids: { + type: "string", + label: "Content Owner Name", + description: "The content owner name for the user. Eg. `MyContentOwnerName`.", + }, + }; + } + + if (idType === constants.ID_TYPE.CHANNEL_ID.value) { + return { + idType: propsFragments.idType, + ids: { + type: "string", + label: "Channel ID", + description: "The channel ID for the user. Eg. `UC_x5XG1OV2P6uZZ5FSM9Ttw`. You can find the ID using the [YouTube Data API](https://developers.google.com/youtube/v3/docs/channels/list).", + }, + }; + } + + return { + idType: propsFragments.idType, + }; + }, + getIdsParam() { + const { + idType, + ids, + } = this; + if (idType === constants.ID_TYPE.CHANNEL.value) { + return "channel==MINE"; + } + if (idType === constants.ID_TYPE.CONTENT_OWNER.value) { + return `contentOwner==${ids}`; + } + if (idType === constants.ID_TYPE.CHANNEL_ID.value) { + return `channel==${ids}`; + } + }, + getFiltersParam() { + const { filters } = this; + const filtersObj = utils.parseJson(filters); + + if (!filtersObj) { + return; + } + + return utils.arrayToCommaSeparatedList( + Object.entries(filtersObj) + .reduce((acc, [ + key, + val, + ]) => [ + ...acc, + `${key}==${val}`, + ], []), + ";", + ); + }, + }, +}; diff --git a/components/youtube_analytics_api/actions/get-video-metrics/get-video-metrics.mjs b/components/youtube_analytics_api/actions/get-video-metrics/get-video-metrics.mjs new file mode 100644 index 0000000000000..e8a66998f4cc7 --- /dev/null +++ b/components/youtube_analytics_api/actions/get-video-metrics/get-video-metrics.mjs @@ -0,0 +1,54 @@ +import common from "../common/reports-query.mjs"; +import utils from "../../common/utils.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; + +export default { + ...common, + key: "youtube_analytics_api-get-video-metrics", + name: "Get Video Metrics", + description: "Retrieve detailed analytics for a specific video. [See the documentation](https://developers.google.com/youtube/analytics/reference/reports/query)", + version: "0.0.1", + type: "action", + props: { + ...common.props, + videoId: { + type: "string", + label: "Video ID", + description: "The ID of the video for which you want to retrieve metrics. Eg. `pd1FJh59zxQ`.", + }, + metrics: propsFragments.metrics, + }, + additionalProps() { + return this.getIdsProps(); + }, + async run({ $ }) { + const { + app, + videoId, + getIdsParam, + startDate, + endDate, + metrics, + dimensions, + sort, + maxResults, + } = this; + + const response = await app.reportsQuery({ + $, + params: { + ids: getIdsParam(), + startDate, + endDate, + metrics: utils.arrayToCommaSeparatedList(metrics), + dimensions: utils.arrayToCommaSeparatedList(dimensions), + sort: utils.arrayToCommaSeparatedList(sort), + maxResults, + filters: `video==${videoId}`, + }, + }); + + $.export("$summary", "Successfully fetched video metrics."); + return response; + }, +}; diff --git a/components/youtube_analytics_api/actions/list-channel-reports/list-channel-reports.mjs b/components/youtube_analytics_api/actions/list-channel-reports/list-channel-reports.mjs new file mode 100644 index 0000000000000..41eccc38e1d25 --- /dev/null +++ b/components/youtube_analytics_api/actions/list-channel-reports/list-channel-reports.mjs @@ -0,0 +1,106 @@ +import common from "../common/reports-query.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; + +export default { + ...common, + key: "youtube_analytics_api-list-channel-reports", + name: "List Channel Reports", + description: "Fetch summary analytics reports for a specified youtube channel. Optional filters include date range and report type. [See the documentation](https://developers.google.com/youtube/analytics/reference/reports/query)", + version: "0.0.1", + type: "action", + additionalProps() { + const { + getIdsProps, + getReportTypeProps, + } = this; + + return { + ...getIdsProps(), + ...getReportTypeProps(), + }; + }, + methods: { + ...common.methods, + getReportTypeProps() { + const { channelReportType } = this; + const { + VIDEO_BASIC_USER_ACTIVITY_STATS, + PLAYLIST_BASIC_STATS, + } = constants.CHANNEL_REPORT_TYPE; + + if (channelReportType === VIDEO_BASIC_USER_ACTIVITY_STATS.value) { + const supportedFilters = VIDEO_BASIC_USER_ACTIVITY_STATS.metadata.filters + .reduce((acc, filter) => ({ + ...acc, + [filter]: "", + }), {}); + + return { + channelReportType: propsFragments.channelReportType, + metrics: { + ...propsFragments.metrics, + options: VIDEO_BASIC_USER_ACTIVITY_STATS.metadata.metrics, + }, + filters: { + ...propsFragments.filters, + description: `**Supported filters: \`${JSON.stringify(supportedFilters)}\`**. ${propsFragments.filters.description}`, + }, + }; + } + + if (channelReportType === PLAYLIST_BASIC_STATS.value) { + const supportedFilters = PLAYLIST_BASIC_STATS.metadata.filters + .reduce((acc, filter) => ({ + ...acc, + [filter]: "", + }), {}); + + return { + channelReportType: propsFragments.channelReportType, + metrics: { + ...propsFragments.metrics, + options: PLAYLIST_BASIC_STATS.metadata.metrics, + }, + filters: { + ...propsFragments.filters, + description: `**Supported filters: \`${JSON.stringify(supportedFilters)}\`**. ${propsFragments.filters.description}`, + }, + }; + } + + return { + channelReportType: propsFragments.channelReportType, + }; + }, + }, + async run({ $ }) { + const { + app, + getIdsParam, + getFiltersParam, + startDate, + endDate, + metrics, + sort, + maxResults, + } = this; + + const response = await app.reportsQuery({ + $, + params: { + ids: getIdsParam(), + startDate, + endDate, + metrics: utils.arrayToCommaSeparatedList(metrics), + filters: getFiltersParam(), + sort: utils.arrayToCommaSeparatedList(sort), + maxResults, + }, + }); + + $.export("$summary", "Successfully fetched channel reports."); + return response; + }, +}; diff --git a/components/youtube_analytics_api/actions/query-custom-analytics/query-custom-analytics.mjs b/components/youtube_analytics_api/actions/query-custom-analytics/query-custom-analytics.mjs new file mode 100644 index 0000000000000..55c8765cd3d4a --- /dev/null +++ b/components/youtube_analytics_api/actions/query-custom-analytics/query-custom-analytics.mjs @@ -0,0 +1,50 @@ +import common from "../common/reports-query.mjs"; +import utils from "../../common/utils.mjs"; +import propsFragments from "../../common/props-fragments.mjs"; + +export default { + ...common, + key: "youtube_analytics_api-query-custom-analytics", + name: "Query Custom Analytics", + description: "Execute a custom analytics query using specified metrics, dimensions, filters, and date ranges. Requires query parameters to configure. [See the documentation](https://developers.google.com/youtube/analytics/reference/reports/query).", + version: "0.0.1", + type: "action", + props: { + ...common.props, + metrics: propsFragments.metrics, + filters: propsFragments.filters, + }, + additionalProps() { + return this.getIdsProps(); + }, + async run({ $ }) { + const { + app, + getIdsParam, + getFiltersParam, + startDate, + endDate, + metrics, + dimensions, + sort, + maxResults, + } = this; + + const response = await app.reportsQuery({ + $, + params: { + ids: getIdsParam(), + startDate, + endDate, + metrics: utils.arrayToCommaSeparatedList(metrics), + dimensions: utils.arrayToCommaSeparatedList(dimensions), + filters: getFiltersParam(), + sort: utils.arrayToCommaSeparatedList(sort), + maxResults, + }, + }); + + $.export("$summary", "Successfully fetched custom analytics data."); + return response; + }, +}; diff --git a/components/youtube_analytics_api/common/constants.mjs b/components/youtube_analytics_api/common/constants.mjs new file mode 100644 index 0000000000000..71be1063c4036 --- /dev/null +++ b/components/youtube_analytics_api/common/constants.mjs @@ -0,0 +1,164 @@ +const METRIC = { + AD_IMPRESSIONS: "adImpressions", + ANNOTATION_CLICKABLE_IMPRESSIONS: "annotationClickableImpressions", + ANNOTATION_CLICKS: "annotationClicks", + ANNOTATION_CLICK_THROUGH_RATE: "annotationClickThroughRate", + ANNOTATION_CLOSABLE_IMPRESSIONS: "annotationClosableImpressions", + ANNOTATION_CLOSES: "annotationCloses", + ANNOTATION_CLOSE_RATE: "annotationCloseRate", + ANNOTATION_IMPRESSIONS: "annotationImpressions", + AUDIENCE_WATCH_RATIO: "audienceWatchRatio", + AVERAGE_VIEW_DURATION: "averageViewDuration", + AVERAGE_VIEW_PERCENTAGE: "averageViewPercentage", + CARD_CLICK_RATE: "cardClickRate", + CARD_CLICKS: "cardClicks", + CARD_IMPRESSIONS: "cardImpressions", + CARD_TEASER_CLICK_RATE: "cardTeaserClickRate", + CARD_TEASER_CLICKS: "cardTeaserClicks", + CARD_TEASER_IMPRESSIONS: "cardTeaserImpressions", + COMMENTS: "comments", + CPM: "cpm", + DISLIKES: "dislikes", + ESTIMATED_AD_REVENUE: "estimatedAdRevenue", + ESTIMATED_MINUTES_WATCHED: "estimatedMinutesWatched", + ESTIMATED_REVENUE: "estimatedRevenue", + GROSS_REVENUE: "grossRevenue", + LIKES: "likes", + MONETIZED_PLAYBACKS: "monetizedPlaybacks", + PLAYBACK_BASED_CPM: "playbackBasedCpm", + PLAYLIST_STARTS: "playlistStarts", + SAVES_ADDED: "savesAdded", + SAVES_REMOVED: "savesRemoved", + SHARES: "shares", + SUBSCRIBERS_GAINED: "subscribersGained", + SUBSCRIBERS_LOST: "subscribersLost", + VIDEOS_ADDED_TO_PLAYLISTS: "videosAddedToPlaylists", + VIDEOS_REMOVED_FROM_PLAYLISTS: "videosRemovedFromPlaylists", + VIEWER_PERCENTAGE: "viewerPercentage", + VIEWS: "views", +}; + +const DIMENSION = { + AD_TYPE: "adType", + AGE_GROUP: "ageGroup", + ASSET: "asset", + AUDIENCE_TYPE: "audienceType", + CHANNEL: "channel", + CLAIMED_STATUS: "claimedStatus", + CONTENT_OWNER: "contentOwner", + COUNTRY: "country", + DAY: "day", + DEVICE_TYPE: "deviceType", + ELAPSED_VIDEO_TIME_RATIO: "elapsedVideoTimeRatio", + GENDER: "gender", + INSIGHT_PLAYBACK_LOCATION_DETAIL: "insightPlaybackLocationDetail", + INSIGHT_PLAYBACK_LOCATION_TYPE: "insightPlaybackLocationType", + INSIGHT_TRAFFIC_SOURCE_DETAIL: "insightTrafficSourceDetail", + INSIGHT_TRAFFIC_SOURCE_TYPE: "insightTrafficSourceType", + LIVE_OR_ON_DEMAND: "liveOrOnDemand", + OPERATING_SYSTEM: "operatingSystem", + PLAYLIST: "playlist", + PROVINCE: "province", + SHARING_SERVICE: "sharingService", + SUBSCRIBED_STATUS: "subscribedStatus", + SUBTITLE_LANGUAGE: "subtitleLanguage", + UPLOADER_TYPE: "uploaderType", + VIDEO: "video", +}; + +const ID_TYPE = { + CHANNEL: { + label: "My Channel", + value: "MINE", + }, + CHANNEL_ID: { + label: "Channel ID", + value: "channelId", + }, + CONTENT_OWNER: { + label: "Content Owner", + value: "contentOwner", + }, +}; + +const CHANNEL_REPORT_TYPE = { + VIDEO_BASIC_USER_ACTIVITY_STATS: { + label: "Basic User Activity Statistics For Video", + value: "basicUserActivityStatsForVideo", + metadata: { + metrics: [ + "views", + "redViews", + "comments", + "likes", + "dislikes", + "videosAddedToPlaylists", + "videosRemovedFromPlaylists", + "shares", + "estimatedMinutesWatched", + "estimatedRedMinutesWatched", + "averageViewDuration", + "averageViewPercentage", + "annotationClickThroughRate", + "annotationCloseRate", + "annotationImpressions", + "annotationClickableImpressions", + "annotationClosableImpressions", + "annotationClicks", + "annotationCloses", + "cardClickRate", + "cardTeaserClickRate", + "cardImpressions", + "cardTeaserImpressions", + "cardClicks", + "cardTeaserClicks", + "subscribersGained", + "subscribersLost", + "estimatedRevenue*", + "estimatedAdRevenue*", + "grossRevenue*", + "estimatedRedPartnerRevenue*", + "monetizedPlaybacks*", + "playbackBasedCpm*", + "adImpressions*", + "cpm*", + ], + filters: [ + "country", + "continent", + "subContinent", + "video", + "group", + ], + }, + }, + PLAYLIST_BASIC_STATS: { + label: "Basic Statistics For Playlist", + value: "basicStatsForPlaylist", + metadata: { + metrics: [ + "views", + "estimatedMinutesWatched", + "averageViewDuration", + "averageTimeInPlaylist", + "playlistAverageViewDuration", + "playlistEstimatedMinutesWatched", + "playlistSaves", + "playlistStarts", + "playlistViews", + "viewsPerPlaylistStart", + ], + filters: [ + "playlist", + "group", + ], + }, + }, +}; + +export default { + METRIC, + DIMENSION, + ID_TYPE, + CHANNEL_REPORT_TYPE, +}; diff --git a/components/youtube_analytics_api/common/props-fragments.mjs b/components/youtube_analytics_api/common/props-fragments.mjs new file mode 100644 index 0000000000000..098547f79d376 --- /dev/null +++ b/components/youtube_analytics_api/common/props-fragments.mjs @@ -0,0 +1,37 @@ +import constants from "./constants.mjs"; + +export default { + idType: { + type: "string", + label: "ID Type", + description: "The type of ID to use for the query. This can be either `My Channel`, `Channel ID`, or `Content Owner`.", + options: Object.values(constants.ID_TYPE), + default: constants.ID_TYPE.CHANNEL.value, + reloadProps: true, + }, + channelReportType: { + type: "string", + label: "Channel Report Type", + description: "The type of report to fetch for the specified YouTube Channel. This selects default dimensions, metrics and filters.", + options: Object.values(constants.CHANNEL_REPORT_TYPE) + .map(({ + // eslint-disable-next-line no-unused-vars + metadata, + ...rest + }) => rest), + default: constants.CHANNEL_REPORT_TYPE.VIDEO_BASIC_USER_ACTIVITY_STATS.value, + reloadProps: true, + }, + metrics: { + type: "string[]", + label: "Metrics", + description: "Metrics, such as `views` or `likes`, `dislikes`. See the documentation for [channel reports](https://developers.google.com/youtube/analytics/channel_reports) or [content owner reports](https://developers.google.com/youtube/analytics/content_owner_reports) for a list of the reports that you can retrieve and the metrics available in each report. (The [Metrics](https://developers.google.com/youtube/reporting#metrics) document contains definitions for all of the metrics.).", + options: Object.values(constants.METRIC), + }, + filters: { + type: "object", + label: "Filters", + description: "A list of filters that should be applied when retrieving YouTube Analytics data. The documentation for [channel reports](https://developers.google.com/youtube/analytics/channel_reports) and [content owner reports](https://developers.google.com/youtube/analytics/content_owner_reports) identifies the dimensions that can be used to filter each report, and the [Dimensions](https://developers.google.com/youtube/analytics/dimsmets/dims) document defines those dimensions.\n\nIf a request uses multiple filters the returned result table will satisfy both filters. For example, a filters parameter value of `{\"video\":\"dMH0bHeiRNg\",\"country\":\"IT\"}` restricts the result set to include data for the given video in Italy.\n\nSpecifying multiple values for a filter\nThe API supports the ability to specify multiple values for the [video](https://developers.google.com/youtube/reporting#supported-reports), [playlist](https://developers.google.com/youtube/reporting#supported-reports), and [channel](https://developers.google.com/youtube/reporting#supported-reports) filters. To do so, specify a separated list of the video, playlist, or channel IDs for which the API response should be filtered. For example, a filters parameter value of `{\"video\":\"pd1FJh59zxQ,Zhawgd0REhA\",\"country\":\"IT\"}` restricts the result set to include data for the given videos in Italy. The parameter value can specify up to 500 IDs. For more details on the filters parameter, see the filters parameter in [Parameters](https://developers.google.com/youtube/analytics/reference/reports/query#Parameters) section.", + optional: true, + }, +}; diff --git a/components/youtube_analytics_api/common/utils.mjs b/components/youtube_analytics_api/common/utils.mjs new file mode 100644 index 0000000000000..2f7e4f69c0054 --- /dev/null +++ b/components/youtube_analytics_api/common/utils.mjs @@ -0,0 +1,57 @@ +import { ConfigurationError } from "@pipedream/platform"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +function arrayToCommaSeparatedList(array, char = ",") { + return parseArray(array)?.join(char); +} + +export default { + parseJson, + parseArray, + arrayToCommaSeparatedList, +}; diff --git a/components/youtube_analytics_api/package.json b/components/youtube_analytics_api/package.json new file mode 100644 index 0000000000000..c19490cbb9ffd --- /dev/null +++ b/components/youtube_analytics_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/youtube_analytics_api", + "version": "0.0.1", + "description": "Pipedream Youtube Analytics API Components", + "main": "youtube_analytics_api.app.mjs", + "keywords": [ + "pipedream", + "youtube_analytics_api" + ], + "homepage": "https://pipedream.com/apps/youtube_analytics_api", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/youtube_analytics_api/youtube_analytics_api.app.mjs b/components/youtube_analytics_api/youtube_analytics_api.app.mjs index 75ef088ab620a..ed86d861eb08d 100644 --- a/components/youtube_analytics_api/youtube_analytics_api.app.mjs +++ b/components/youtube_analytics_api/youtube_analytics_api.app.mjs @@ -1,11 +1,64 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "youtube_analytics_api", - propDefinitions: {}, + propDefinitions: { + endDate: { + type: "string", + label: "End Date", + description: "The end date for fetching YouTube Analytics data. The value should be in `YYYY-MM-DD` format. The API response contains data up until the last day for which all metrics in the query are available at the time of the query. So, for example, if the request specifies an end date of July 5, 2017, and values for all of the requested metrics are only available through July 3, 2017, that will be the last date for which data is included in the response. (That is true even if data for some of the requested metrics is available for July 4, 2017.)", + }, + startDate: { + type: "string", + label: "Start Date", + description: "The start date for fetching YouTube Analytics data. The value should be in `YYYY-MM-DD` format.", + }, + dimensions: { + type: "string[]", + label: "Dimensions", + description: "A list of YouTube Analytics dimensions, such as `video` or `ageGroup`, `gender`. See the documentation for [channel reports](https://developers.google.com/youtube/analytics/channel_reports) or [content owner reports](https://developers.google.com/youtube/analytics/content_owner_reports) for a list of the reports that you can retrieve and the dimensions used for those reports. (The [Dimensions](https://developers.google.com/youtube/reporting#dimensions) document contains definitions for all of the dimensions.).", + optional: true, + options: Object.values(constants.DIMENSION), + }, + sort: { + type: "string[]", + label: "Sort", + description: "A list of dimensions or metrics that determine the sort order for YouTube Analytics data. By default the sort order is ascending. The `-` prefix causes descending sort order. Eg. `-views`.", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of rows to include in the response.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `https://youtubeanalytics.googleapis.com/v2${path}`; + }, + getHeaders() { + return { + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + "Accept": "application/json", + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + reportsQuery(args = {}) { + return this._makeRequest({ + path: "/reports", + ...args, + }); }, }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c3319d9e5ff8..59d42e8d60e73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1692,8 +1692,7 @@ importers: specifier: ^3.0.3 version: 3.0.3 - components/charthop: - specifiers: {} + components/charthop: {} components/chartmogul: dependencies: @@ -7278,8 +7277,7 @@ importers: specifier: ^1.2.0 version: 1.6.6 - components/opensrs: - specifiers: {} + components/opensrs: {} components/openweather_api: dependencies: @@ -11881,8 +11879,7 @@ importers: specifier: ^1.2.0 version: 1.6.6 - components/xverify: - specifiers: {} + components/xverify: {} components/y_gy: dependencies: @@ -11953,6 +11950,12 @@ importers: specifier: ^1.2.0 version: 1.6.6 + components/youtube_analytics_api: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 + components/youtube_data_api: dependencies: '@googleapis/youtube': @@ -30938,8 +30941,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: