diff --git a/Node/youtube/README.md b/Node/youtube/README.md new file mode 100644 index 000000000..0767d1005 --- /dev/null +++ b/Node/youtube/README.md @@ -0,0 +1,81 @@ +# YouTube: Get information about a YouTube channel + +This quickstart demonstrates how to query the +[YouTube Data API](https://developers.google.com/youtube/v3) using **Cloud +Functions for Firebase** with an HTTPS trigger. + +## Introduction + +The function `getChannelInfo` returns information about a Youtube channel. By +default it will return information about the +[Firebase YouTube channel](https://www.youtube.com/user/Firebase), but you can pass it a +`channelId` URL Query parameter to query any channel you'd like. + +## Setup + +### Get a YouTube API Key + +1. Create a Firebase Project on the + [Firebase Console](https://console.firebase.google.com) if you don't already have a project you want to use. + 1. Upgrade your Firebase project to the + [Blaze "pay as you go" plan](https://firebase.google.com/pricing) +1. Enable the Youtube API by visiting the + [API console](http://console.cloud.google.com/marketplace/product/google/youtube.googleapis.com), + selecting your Firebase project, and clicking "ENABLE". + 1. Once the API is enabled, visit the + [credentials tab](http://console.cloud.google.com/apis/api/youtube.googleapis.com/credentials) + and click "CREATE CREDENTIALS" to create a YouTube API key. + +### Clone and configure the function + +1. Install the Firebase CLI and log in: + ``` + npm install --global firebase-tools + + firebase login + ``` +1. Clone or download this repo and open the `youtube` directory. +1. `cd` into the `functions` directory and install dependencies with `npm install` +1. Set up your Firebase project by running `firebase use --add` with the + Firebase CLI, select your Project ID and follow the instructions. +1. Set the YouTube API key as an environment variable: + ```bash + firebase functions:config:set youtube.key="THE API KEY" + ``` + +### Run your function locally with the Firebase Emulator Suite + +1. Set up the Firebase emulators with your config ([docs](https://firebase.google.com/docs/functions/local-emulator#set_up_functions_configuration_optional)): + ```bash + cd functions + + firebase functions:config:get > .runtimeconfig.json + ``` +1. Run the following command to start the emulator: + ```bash + firebase emulators:start --only functions + ``` +1. Check the emulator output to find the URL of the `getChannelInfo` function. It will looks something like `http://localhost:5001/my-project-id/us-central1/getChannelInfo` +1. Via CURL or in your browser, visit the URL that the function is running at. Optionally, add a query string `?channelId=SOME_CHANNEL_ID` to the end of the URL. +1. You should get a JSON response with information about the YouTube channel! + + +## Deploy the app to prod + +Deploy to Firebase using the following command: + +```bash +firebase deploy +``` + +This deploys and activates the `getChannelInfo` function. + +> The first time you call `firebase deploy` on a new project with Functions will take longer than usual. + +## Modify it to your needs + +Now that you've got this sample working, modify it to work for your use case! Some ideas: + +- Check out the other things you can query with the [YouTube Data API](https://developers.google.com/youtube/v3/docs) +- Convert `getChannelInfo` function to a scheduled function, and write the new latest videos for a channel into Firestore or Realtime Database +- ...anything else you can think of! \ No newline at end of file diff --git a/Node/youtube/firebase.json b/Node/youtube/firebase.json new file mode 100644 index 000000000..da729ff0b --- /dev/null +++ b/Node/youtube/firebase.json @@ -0,0 +1,17 @@ +{ + "functions": { + "codebase": "youtube", + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run lint" + ], + "source": "functions" + }, + "emulators": { + "functions": { + "port": 5001 + }, + "ui": { + "enabled": true + } + } +} diff --git a/Node/youtube/functions/.eslintrc.js b/Node/youtube/functions/.eslintrc.js new file mode 100644 index 000000000..14cf70935 --- /dev/null +++ b/Node/youtube/functions/.eslintrc.js @@ -0,0 +1,30 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + root: true, + env: { + es2020: true, + node: true, + }, + extends: [ + "eslint:recommended", + "google", + ], + rules: { + quotes: ["error", "double"], + }, +}; diff --git a/Node/youtube/functions/index.js b/Node/youtube/functions/index.js new file mode 100644 index 000000000..338de9e3a --- /dev/null +++ b/Node/youtube/functions/index.js @@ -0,0 +1,101 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const {onCall, HttpsError} = require("firebase-functions/v2/https"); +const {defineString, defineSecret} = require("firebase-functions/params"); + +const {google} = require("googleapis"); + +const youtubeKey = defineSecret("YOUTUBE_API_KEY"); +const defaultChannelId = defineString("DEFAULT_CHANNEL_ID", { + default: "UCP4bf6IHJJQehibu6ai__cg", +}); + +exports.getChannelInfo = onCall( + {secrets: [youtubeKey]}, + + async (request, response) => { + const youtube = google.youtube({ + version: "v3", + auth: youtubeKey.value(), + }); + const channelId = request.data.channelId || defaultChannelId; + + const channelInfo = {id: channelId}; + + // Fetch channel information + let channelData; + try { + // tell the client app we're starting + response.sendChunk({status: "fetching channel info", channelInfo}); + + // https://developers.google.com/youtube/v3/docs/channels/list + const {data} = await youtube.channels.list({ + part: "snippet,statistics", + id: channelId, + maxResults: 1, + }); + channelData = data; + } catch (error) { + throw new HttpsError("internal", "Failed to fetch channel data."); + } + + if (!channelData.items || channelData.items.length !== 1) { + throw new HttpsError( + "invalid-argument", + `Channel with ID ${channelId} not found.`, + ); + } + + const channel = channelData.items[0]; + (channelInfo.channelTitle = channel.snippet.title), + (channelInfo.channelDescription = channel.snippet.description), + (channelInfo.subscriberCount = channel.statistics.subscriberCount), + // send latest data to the client app + response.sendChunk({status: "found channel info", channelInfo}); + + // Fetch the channel's latest videos + let videoData; + try { + // tell the client app we're starting to fetch videos + response.sendChunk({status: "fetching latest videos", channelInfo}); + // https://developers.google.com/youtube/v3/docs/search/list + const {data} = await youtube.search.list({ + part: "id, snippet", + order: "date", + channelId, + maxResults: 3, + }); + videoData = data; + } catch (error) { + throw new HttpsError("internal", "Failed to fetch video data."); + } + const videos = (videoData.items || []).map((video) => ({ + videoTitle: video.snippet.title, + videoUrl: `https://www.youtube.com/watch?v=${video.id.videoId}`, + videoDescription: video.snippet.description, + })); + + channelInfo.recentVideos = videos; + // send the latest data to the client app + response.sendChunk({ + status: `found ${videos.length} videos`, + channelInfo, + }); + + return channelInfo; + }, +); diff --git a/Node/youtube/functions/package.json b/Node/youtube/functions/package.json new file mode 100644 index 000000000..be5b93c8a --- /dev/null +++ b/Node/youtube/functions/package.json @@ -0,0 +1,26 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": { + "lint": "eslint .", + "serve": "firebase emulators:start --only functions", + "shell": "firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "18" + }, + "main": "index.js", + "dependencies": { + "firebase-admin": "^11.9.0", + "firebase-functions": "^4.4.1", + "googleapis": "^66.0.0" + }, + "devDependencies": { + "eslint": "^8.40.0", + "firebase-functions-test": "^3.1.0" + }, + "private": true +}