├── .flake8 ├── requirements.txt ├── .isort.cfg ├── Makefile ├── run_pylint.py ├── .editorconfig ├── src └── autogpt_youtube │ ├── functions.py │ ├── youtube_functions.py │ ├── youtube_download.py │ ├── analyze.py │ ├── youtube_api.py │ └── __init__.py ├── .github └── workflows │ └── test-plugin-installation.yml ├── pyproject.toml ├── .pre-commit-config.yaml ├── helpers.sh ├── .coveragerc ├── helpers.bat ├── LICENSE ├── README.md ├── .sourcery.yaml ├── .gitignore └── pylintrc /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203 4 | exclude = 5 | .tox, 6 | __pycache__, 7 | *.pyc, 8 | .env 9 | venv/* 10 | .venv/* 11 | reports/* 12 | dist/* 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black 2 | isort 3 | flake8 4 | pylint 5 | wheel 6 | setuptools 7 | build 8 | twine 9 | requests 10 | auto_gpt_plugin_template 11 | yt-dlp 12 | SpeechRecognition 13 | pydub 14 | youtube_transcript_api -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile = black 3 | multi_line_output = 3 4 | include_trailing_comma = true 5 | force_grid_wrap = 0 6 | use_parentheses = true 7 | ensure_newline_before_comments = true 8 | line_length = 88 9 | sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 10 | skip = .tox,__pycache__,*.pyc,venv*/*,reports,venv,env,node_modules,.env,.venv,dist 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(OS),Windows_NT) 2 | os := win 3 | SCRIPT_EXT := .bat 4 | SHELL_CMD := cmd /C 5 | else 6 | os := nix 7 | SCRIPT_EXT := .sh 8 | SHELL_CMD := bash 9 | endif 10 | 11 | helpers = @$(SHELL_CMD) helpers$(SCRIPT_EXT) $1 12 | 13 | clean: helpers$(SCRIPT_EXT) 14 | $(call helpers,clean) 15 | 16 | qa: helpers$(SCRIPT_EXT) 17 | $(call helpers,qa) 18 | 19 | style: helpers$(SCRIPT_EXT) 20 | $(call helpers,style) 21 | 22 | .PHONY: clean qa style 23 | -------------------------------------------------------------------------------- /run_pylint.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://stackoverflow.com/questions/49100806/ 3 | pylint-and-subprocess-run-returning-exit-status-28 4 | """ 5 | import subprocess 6 | 7 | cmd = " pylint src\\**\\*" 8 | try: 9 | subprocComplete = subprocess.run( 10 | cmd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE 11 | ) 12 | print(subprocComplete.stdout.decode("utf-8")) 13 | except subprocess.CalledProcessError as err: 14 | print(err.output.decode("utf-8")) 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file 2 | root = true 3 | 4 | # Set default charset 5 | [*] 6 | charset = utf-8 7 | 8 | # Use black formatter for python files 9 | [*.py] 10 | profile = black 11 | 12 | # Set defaults for windows and batch filess 13 | [*.bat] 14 | end_of_line = crlf 15 | indent_style = space 16 | indent_size = 2 17 | 18 | # Set defaults for shell scripts 19 | [*.sh] 20 | end_of_line = lf 21 | trim_trailing_whitespace = true 22 | insert_final_newline = false 23 | 24 | # Set defaults for Makefiles 25 | [Makefile] 26 | end_of_line = lf 27 | indent_style = tab 28 | indent_size = 4 29 | trim_trailing_whitespace = true 30 | insert_final_newline = true -------------------------------------------------------------------------------- /src/autogpt_youtube/functions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def convert_ISO8601_to_seconds(duration: str) -> int: 4 | """Convert ISO 8601 duration format to seconds. 5 | 6 | Args: 7 | duration (str): The duration in ISO 8601 format. 8 | 9 | Returns: 10 | int: The duration in seconds. 11 | """ 12 | 13 | duration = duration.replace("PT", "") 14 | 15 | hours, minutes, seconds = 0, 0, 0 16 | if "H" in duration: 17 | hours = int(duration.split("H")[0]) 18 | duration = duration.split("H")[1] 19 | if "M" in duration: 20 | minutes = int(duration.split("M")[0]) 21 | duration = duration.split("M")[1] 22 | if "S" in duration: 23 | seconds = int(duration.split("S")[0]) 24 | return hours * 3600 + minutes * 60 + seconds -------------------------------------------------------------------------------- /.github/workflows/test-plugin-installation.yml: -------------------------------------------------------------------------------- 1 | name: Test installation of plugin against the PR 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | test-plugin-installation: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Install Kurtosis 13 | run: | 14 | echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list 15 | sudo apt update 16 | sudo apt install kurtosis-cli 17 | - name: Install plugins 18 | run: | 19 | set -euo pipefail 20 | kurtosis run github.com/kurtosis-tech/autogpt-package '{"OPENAI_API_KEY": "test", "ALLOWLISTED_PLUGINS": "AutoGPT_YouTube", "__plugin_branch_to_use": '\"${{ github.head_ref}}\"', "__plugin_repo_to_use":'\"${{ github.event.pull_request.head.repo.full_name }}\"'}' 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "auto_gpt_plugin_template" 7 | version = "0.0.2" 8 | authors = [ 9 | { name="Torantulino", email="34168009+BillSchumacher@users.noreply.github.com" }, 10 | ] 11 | description = "The template plugin for Auto-GPT." 12 | readme = "README.md" 13 | requires-python = ">=3.8" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | dependencies = ["abstract-singleton"] 20 | 21 | [project.urls] 22 | "Homepage" = "https://github.com/Torantulino/Auto-GPT" 23 | "Bug Tracker" = "https://github.com/Torantulino/Auto-GPT" 24 | 25 | [tool.black] 26 | line-length = 88 27 | target-version = ['py38'] 28 | include = '\.pyi?$' 29 | extend-exclude = "" 30 | 31 | [tool.isort] 32 | profile = "black" 33 | 34 | [tool.pylint.messages_control] 35 | disable = "C0330, C0326" 36 | 37 | [tool.pylint.format] 38 | max-line-length = "88" -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/sourcery-ai/sourcery 3 | rev: v1.1.0 # Get the latest tag from https://github.com/sourcery-ai/sourcery/tags 4 | hooks: 5 | - id: sourcery 6 | 7 | - repo: git://github.com/pre-commit/pre-commit-hooks 8 | rev: v0.9.2 9 | hooks: 10 | - id: check-added-large-files 11 | args: [ '--maxkb=500' ] 12 | - id: check-byte-order-marker 13 | - id: check-case-conflict 14 | - id: check-merge-conflict 15 | - id: check-symlinks 16 | - id: debug-statements 17 | 18 | - repo: local 19 | hooks: 20 | - id: isort 21 | name: isort-local 22 | entry: isort 23 | language: python 24 | types: [ python ] 25 | exclude: .+/(dist|.venv|venv|build)/.+ 26 | pass_filenames: true 27 | - id: black 28 | name: black-local 29 | entry: black 30 | language: python 31 | types: [ python ] 32 | exclude: .+/(dist|.venv|venv|build)/.+ 33 | pass_filenames: true -------------------------------------------------------------------------------- /helpers.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | clean() { 4 | # Remove build artifacts and temporary files 5 | rm -rf build 2>/dev/null || true 6 | rm -rf dist 2>/dev/null || true 7 | rm -rf __pycache__ 2>/dev/null || true 8 | rm -rf *.egg-info 2>/dev/null || true 9 | rm -rf **/*.egg-info 2>/dev/null || true 10 | rm -rf *.pyc 2>/dev/null || true 11 | rm -rf **/*.pyc 2>/dev/null || true 12 | rm -rf reports 2>/dev/null || true 13 | } 14 | 15 | qa() { 16 | # Run static analysis tools 17 | flake8 . 18 | python run_pylint.py 19 | } 20 | 21 | style() { 22 | # Format code 23 | isort . 24 | black --exclude=".*\/*(dist|venv|.venv|test-results)\/*.*" . 25 | } 26 | 27 | if [ "$1" = "clean" ]; then 28 | echo Removing build artifacts and temporary files... 29 | clean 30 | elif [ "$1" = "qa" ]; then 31 | echo Running static analysis tools... 32 | qa 33 | elif [ "$1" = "style" ]; then 34 | echo Running code formatters... 35 | style 36 | else 37 | echo "Usage: $0 [clean|qa|style]" 38 | exit 1 39 | fi 40 | 41 | echo Done! 42 | echo 43 | exit 0 -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [coverage:run] 2 | branch = True 3 | plugins = 4 | omit = 5 | # omit anything in a .local directory anywhere 6 | */.local/* 7 | 8 | [run] 9 | branch = True 10 | plugins = 11 | omit = 12 | # omit anything in a .local directory anywhere 13 | */.local/* 14 | 15 | [paths] 16 | source = . 17 | 18 | 19 | [report] 20 | # Regexes for lines to exclude from consideration 21 | 22 | omit = 23 | # omit anything in a .local directory anywhere 24 | */.local/* 25 | 26 | exclude_lines = 27 | # Have to re-enable the standard pragma 28 | pragma: no cover 29 | # Don't complain about missing debug-only code: 30 | def __repr__ 31 | if self\.debug 32 | 33 | # Don't complain if tests don't hit defensive assertion code: 34 | raise AssertionError 35 | raise NotImplementedError 36 | 37 | # Don't complain if non-runnable code isn't run: 38 | if 0: 39 | if __name__ == .__main__.: 40 | 41 | # Don't complain about abstract methods, they aren't run: 42 | @(abc\.)?abstractmethod 43 | 44 | ignore_errors = True 45 | -------------------------------------------------------------------------------- /helpers.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | if "%1" == "clean" ( 4 | echo Removing build artifacts and temporary files... 5 | call :clean 6 | ) else if "%1" == "qa" ( 7 | echo Running static analysis tools... 8 | call :qa 9 | ) else if "%1" == "style" ( 10 | echo Running code formatters... 11 | call :style 12 | ) else ( 13 | echo Usage: %0 [clean^|qa^|style] 14 | exit /b 1 15 | ) 16 | 17 | exit /b 0 18 | 19 | :clean 20 | rem Remove build artifacts and temporary files 21 | @del /s /q build 2>nul 22 | @del /s /q dist 2>nul 23 | @del /s /q __pycache__ 2>nul 24 | @del /s /q *.egg-info 2>nul 25 | @del /s /q **\*.egg-info 2>nul 26 | @del /s /q *.pyc 2>nul 27 | @del /s /q **\*.pyc 2>nul 28 | @del /s /q reports 2>nul 29 | echo Done! 30 | exit /b 0 31 | 32 | :qa 33 | rem Run static analysis tools 34 | @flake8 . 35 | @python run_pylint.py 36 | echo Done! 37 | exit /b 0 38 | 39 | :style 40 | rem Format code 41 | @isort . 42 | @black --exclude=".*\/*(dist|venv|.venv|test-results)\/*.*" . 43 | echo Done! 44 | exit /b 0 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Toran Bruce Richards 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/autogpt_youtube/youtube_functions.py: -------------------------------------------------------------------------------- 1 | import youtube_transcript_api 2 | import nltk 3 | import os 4 | 5 | 6 | def get_youtube_transcript(url: str) -> str: 7 | """Get the transcript of a YouTube video. 8 | 9 | Args: 10 | url (str): The URL of the YouTube video. 11 | 12 | Returns: 13 | str: The transcript of the YouTube video. 14 | """ 15 | # get the video id 16 | video_id = url.split("v=")[1] 17 | 18 | # get the transcript 19 | transcript = youtube_transcript_api.YouTubeTranscriptApi.get_transcript(video_id) 20 | 21 | # convert the transcript to a string 22 | transcript = "\n".join([line["text"] for line in transcript]) 23 | 24 | # check if the transcript has more than 2500 tokens 25 | tokenized_text = nltk.tokenize.word_tokenize(transcript) 26 | 27 | MAX_TOKENS = 1000 28 | 29 | if len(tokenized_text) <= MAX_TOKENS: 30 | return transcript 31 | 32 | # save the transcript to a file in the folder auto_gpt_workspace and name it with the pattern: youtube_transcript_{video_id}.txt 33 | file_name = f"youtube_transcript_{video_id}.txt" 34 | 35 | path = os.path.join("autogpt", "auto_gpt_workspace", file_name) 36 | 37 | with open(path, "w") as f: 38 | 39 | f.write(transcript) 40 | 41 | return f"Transcript saved to {file_name}" 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auto-GPT-Plugin-Template 2 | A Plugin for [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) that adds YouTube support. 3 | 4 | ## Features 5 | - [x] Download YouTube videos 6 | - [x] Download YouTube audio 7 | - [x] Search YouTube 8 | - [x] Download YouTube Subtitles (Transcription) 9 | - [x] Fetch comments of a YouTube video 10 | - [x] Get video information 11 | 12 | ## Requirements 13 | - Python 14 | - AutoGPT 15 | - ffmpeg 16 | 17 | ### Plugin Installation Steps 18 | 19 | 1. **Clone or download the plugin repository:** 20 | Clone the plugin repository, or download the repository as a zip file. 21 | 22 | ![Download Zip](https://i.imgur.com/dvGqLMX.png) 23 | 24 | 2. **Install the plugin's dependencies:** 25 | Navigate to the plugin's folder in your terminal, and run the following command to install any required dependencies: 26 | 27 | ``` shell 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | 3. **Package the plugin as a Zip file:** 32 | If you cloned the repository, compress the plugin folder as a Zip file. 33 | 34 | 4. **Copy the plugin's Zip file:** 35 | Place the plugin's Zip file in the `plugins` folder of the Auto-GPT repository. 36 | 37 | 5. **Allowlist the plugin (optional):** 38 | Add the plugin's class name to the `ALLOWLISTED_PLUGINS` in the `.env` file to avoid being prompted with a warning when loading the plugin: 39 | 40 | ``` shell 41 | ALLOWLISTED_PLUGINS=AutoGPT_YouTube,example-plugin1,example-plugin2 42 | ``` 43 | 44 | If the plugin is not allowlisted, you will be warned before it's loaded. 45 | 46 | 6. **Configure the plugin:** 47 | Add the plugin's configuration to the `.env` file: 48 | 49 | ``` shell 50 | ################################################################################ 51 | ### YOUTUBE 52 | ################################################################################ 53 | 54 | YOUTUBE_API_KEY=your-api-key 55 | ``` 56 | -------------------------------------------------------------------------------- /.sourcery.yaml: -------------------------------------------------------------------------------- 1 | # 🪄 This is your project's Sourcery configuration file. 2 | 3 | # You can use it to get Sourcery working in the way you want, such as 4 | # ignoring specific refactorings, skipping directories in your project, 5 | # or writing custom rules. 6 | 7 | # 📚 For a complete reference to this file, see the documentation at 8 | # https://docs.sourcery.ai/Configuration/Project-Settings/ 9 | 10 | # This file was auto-generated by Sourcery on 2023-02-25 at 21:07. 11 | 12 | version: '1' # The schema version of this config file 13 | 14 | ignore: # A list of paths or files which Sourcery will ignore. 15 | - .git 16 | - venv 17 | - .venv 18 | - build 19 | - dist 20 | - env 21 | - .env 22 | - .tox 23 | 24 | rule_settings: 25 | enable: 26 | - default 27 | - gpsg 28 | disable: [] # A list of rule IDs Sourcery will never suggest. 29 | rule_types: 30 | - refactoring 31 | - suggestion 32 | - comment 33 | python_version: '3.9' # A string specifying the lowest Python version your project supports. Sourcery will not suggest refactorings requiring a higher Python version. 34 | 35 | # rules: # A list of custom rules Sourcery will include in its analysis. 36 | # - id: no-print-statements 37 | # description: Do not use print statements in the test directory. 38 | # pattern: print(...) 39 | # language: python 40 | # replacement: 41 | # condition: 42 | # explanation: 43 | # paths: 44 | # include: 45 | # - test 46 | # exclude: 47 | # - conftest.py 48 | # tests: [] 49 | # tags: [] 50 | 51 | # rule_tags: {} # Additional rule tags. 52 | 53 | # metrics: 54 | # quality_threshold: 25.0 55 | 56 | # github: 57 | # labels: [] 58 | # ignore_labels: 59 | # - sourcery-ignore 60 | # request_review: author 61 | # sourcery_branch: sourcery/{base_branch} 62 | 63 | # clone_detection: 64 | # min_lines: 3 65 | # min_duplicates: 2 66 | # identical_clones_only: false 67 | 68 | # proxy: 69 | # url: 70 | # ssl_certs_file: 71 | # no_ssl_verify: false 72 | -------------------------------------------------------------------------------- /src/autogpt_youtube/youtube_download.py: -------------------------------------------------------------------------------- 1 | import yt_dlp as yt 2 | from . import AutoGPT_YouTube 3 | import os 4 | 5 | plugin = AutoGPT_YouTube() 6 | 7 | def download_youtube_video(url: str, output_file: str) -> str: 8 | """Download a youtube video to mp4 format. 9 | 10 | Args: 11 | url (str): The url of the video to download. 12 | output_file (str): The output path. 13 | 14 | Returns: 15 | str: status message 16 | """ 17 | 18 | if output_file.endswith(".mp4") is False: 19 | output_file += ".mp4" 20 | 21 | outtmpl = output_file.removesuffix(".mp4") 22 | outtmpl = os.path.join(plugin.workspace_path, outtmpl) 23 | 24 | # download the video 25 | ydl_opts = { 26 | "format": "bestvideo+bestaudio/best", 27 | "outtmpl": outtmpl, 28 | } 29 | with yt.YoutubeDL(ydl_opts) as ydl: 30 | ydl.download([url]) 31 | 32 | return f"Downloaded the video from {url} to {output_file}" 33 | 34 | 35 | def download_youtube_audio(url: str, output_file: str) -> str: 36 | """Download a youtube video to mp3 format. 37 | 38 | Args: 39 | url (str): The url of the video to download. 40 | output_file (str): The output path. 41 | 42 | Returns: 43 | str: status message 44 | """ 45 | 46 | if output_file.endswith(".mp3") is False: 47 | output_file += ".mp3" 48 | 49 | outtmpl = output_file.removesuffix(".mp3") 50 | outtmpl = os.path.join(plugin.workspace_path, outtmpl) 51 | 52 | # download the video 53 | ydl_opts = { 54 | "format": "bestaudio/best", 55 | "outtmpl": outtmpl, 56 | "postprocessors": [ 57 | { 58 | "key": "FFmpegExtractAudio", 59 | "preferredcodec": "mp3", 60 | "preferredquality": "192", 61 | } 62 | ], 63 | } 64 | with yt.YoutubeDL(ydl_opts) as ydl: 65 | ydl.download([url]) 66 | 67 | return f"Downloaded the audio from {url} to {output_file}" -------------------------------------------------------------------------------- /src/autogpt_youtube/analyze.py: -------------------------------------------------------------------------------- 1 | import speech_recognition as sr 2 | from pydub import AudioSegment 3 | from . import AutoGPT_YouTube 4 | 5 | import os 6 | 7 | plugin = AutoGPT_YouTube() 8 | 9 | def get_audio_duration(audio_file: str) -> str: 10 | """ 11 | Get the duration of an audio file in seconds. 12 | 13 | Args: 14 | audio_file (str): The path to the audio file (mp3 format) 15 | 16 | Returns: 17 | float: The duration of the audio file in seconds. 18 | """ 19 | 20 | audio_file = os.path.join(plugin.workspace_path, audio_file) 21 | 22 | audio_file = AudioSegment.from_file(audio_file, format="mp3") 23 | return f"The duration of the file {audio_file} is {len(audio_file) / 1000} seconds." 24 | 25 | 26 | def audio_to_text(audio_file: str) -> str: 27 | """ 28 | Convert an audio file to text using SpeechRecognition and Pydub. 29 | 30 | Args: 31 | audio_file (str): The path to the audio file (mp3 format) 32 | 33 | Returns: 34 | str: The transcribed text. 35 | """ 36 | 37 | audio_file = os.path.join(plugin.workspace_path, audio_file) 38 | 39 | 40 | r = sr.Recognizer() 41 | 42 | # Load audio file using Pydub 43 | audio_file = AudioSegment.from_file(audio_file, format="mp3") 44 | audio = audio_file.export(format="wav") 45 | print("Audio file loaded and converted to wav format.") 46 | 47 | # Convert audio to text using SpeechRecognition 48 | with sr.AudioFile(audio) as source: 49 | audio_data = r.record(source) 50 | print("Transcribing audio file using Google... This may take a while.") 51 | text = r.recognize_google(audio_data) 52 | 53 | return text 54 | 55 | 56 | 57 | def calculate_engagement_rate(likes: int, comments: int, views: int) -> float: 58 | """ 59 | Calculate the engagement rate of a YouTube video. 60 | 61 | Args: 62 | likes (int): The number of likes. 63 | comments (int): The number of comments. 64 | views (int): The number of views. 65 | 66 | Returns: 67 | float: The engagement rate. 68 | """ 69 | 70 | return (likes + comments) / views -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /src/autogpt_youtube/youtube_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import google.auth 3 | from googleapiclient.discovery import build 4 | 5 | from .functions import convert_ISO8601_to_seconds 6 | from .analyze import calculate_engagement_rate 7 | 8 | # Get the API key from environment variable 9 | API_KEY = os.environ.get("YOUTUBE_API_KEY") 10 | 11 | # Build the YouTube API client 12 | youtube = build("youtube", "v3", developerKey=API_KEY) 13 | 14 | def search_youtube(query: str, max_results: int = 10) -> list: 15 | """Search for a query on youtube. 16 | 17 | Args: 18 | query (str): The query to search for. 19 | max_results (int, optional): The maximum number of results to return. Defaults to 10. 20 | 21 | Returns: 22 | list: The search results. 23 | """ 24 | # Call the search.list method to retrieve search results 25 | search_response = youtube.search().list( 26 | q=query, 27 | type="video", 28 | part="id,snippet", 29 | maxResults=max_results 30 | ).execute() 31 | 32 | # Convert the search results to a list of dictionaries 33 | results = [] 34 | for search_result in search_response.get("items", []): 35 | try: 36 | results.append( 37 | { 38 | "title": search_result["snippet"]["title"], 39 | "channel": search_result["snippet"]["channelTitle"], 40 | "url": f"https://www.youtube.com/watch?v={search_result['id']['videoId']}" 41 | } 42 | ) 43 | except KeyError: 44 | pass 45 | 46 | return results 47 | 48 | 49 | def get_youtube_comments(url: str, max_results: int = 15) -> list: 50 | """Get the comments of a YouTube video. 51 | 52 | Args: 53 | url (str): The URL of the YouTube video. 54 | max_results (int, optional): The maximum number of results to return. Defaults to 15. 55 | 56 | Returns: 57 | list: The comments of the YouTube video. 58 | """ 59 | # Get the video ID from the video URL 60 | video_id = url.split("v=")[1] 61 | 62 | # Retrieve the comments for the video 63 | comments = [] 64 | next_page_token = '' 65 | next_page_counter = 0 66 | 67 | MAX_NEXT_PAGES = 100 68 | 69 | while True: 70 | next_page_counter += 1 71 | if next_page_counter > MAX_NEXT_PAGES: 72 | break 73 | 74 | request = youtube.commentThreads().list( 75 | part='snippet', 76 | videoId=video_id, 77 | textFormat='plainText', 78 | pageToken=next_page_token 79 | ) 80 | response = request.execute() 81 | 82 | # Add the comments to the list 83 | for item in response['items']: 84 | comment = item['snippet']['topLevelComment']['snippet'] 85 | comments.append({ 86 | 'author': comment['authorDisplayName'], 87 | 'date': comment['publishedAt'], 88 | 'comment': comment['textDisplay'], 89 | 'likes': comment['likeCount'], 90 | }) 91 | 92 | # Check if there are more comments to retrieve 93 | if 'nextPageToken' in response: 94 | next_page_token = response['nextPageToken'] 95 | else: 96 | break 97 | 98 | # sort the list of comments 99 | best_comments = sorted(comments, key=lambda k: k['likes'], reverse=True) 100 | 101 | return best_comments[:max_results] 102 | 103 | 104 | 105 | def get_youtube_video_info(url: str): 106 | """Get the information of a YouTube video. 107 | 108 | Args: 109 | url (str): The URL of the YouTube video. 110 | 111 | Returns: 112 | dict: The information of the YouTube video. 113 | """ 114 | # Get the video ID from the video URL 115 | video_id = url.split("v=")[1] 116 | 117 | # Retrieve the comments for the video 118 | video_info = youtube.videos().list( 119 | part="snippet,contentDetails,statistics", 120 | id=video_id 121 | ).execute() 122 | 123 | # Convert the search results to a list of dictionaries 124 | results = [] 125 | for video in video_info.get("items", []): 126 | try: 127 | 128 | # convert video duration from ISO 8601 format to seconds 129 | duration = convert_ISO8601_to_seconds(video["contentDetails"]["duration"]) 130 | 131 | # calculate the engagement rate 132 | engagement_rate = calculate_engagement_rate( 133 | likes=int(video["statistics"]["likeCount"]), 134 | comments=int(video["statistics"]["commentCount"]), 135 | views=int(video["statistics"]["viewCount"]), 136 | ) 137 | 138 | 139 | results.append( 140 | { 141 | "published_at": video["snippet"]["publishedAt"], 142 | "channel_id": video["snippet"]["channelId"], 143 | "title": video["snippet"]["title"], 144 | "description": video["snippet"]["description"], 145 | "thumbnail": video["snippet"]["thumbnails"]["high"]["url"], 146 | "channel_title": video["snippet"]["channelTitle"], 147 | "tags": video["snippet"]["tags"], 148 | "duration": duration, 149 | "view_count": video["statistics"]["viewCount"], 150 | "like_count": video["statistics"]["likeCount"], 151 | "comment_count": video["statistics"]["commentCount"], 152 | "engagement_rate": engagement_rate, 153 | } 154 | ) 155 | except KeyError: 156 | pass 157 | if results[0] == None: 158 | return None 159 | else: 160 | return results[0] -------------------------------------------------------------------------------- /src/autogpt_youtube/__init__.py: -------------------------------------------------------------------------------- 1 | """This is a template for Auto-GPT plugins.""" 2 | from typing import Any, Dict, List, Optional, Tuple, TypeVar, TypedDict 3 | import os 4 | 5 | from auto_gpt_plugin_template import AutoGPTPluginTemplate 6 | 7 | PromptGenerator = TypeVar("PromptGenerator") 8 | 9 | 10 | class Message(TypedDict): 11 | role: str 12 | content: str 13 | 14 | 15 | class AutoGPT_YouTube(AutoGPTPluginTemplate): 16 | """ 17 | This is a plugin for Auto-GPT which enables access to certain YouTube features. 18 | """ 19 | 20 | def __init__(self): 21 | super().__init__() 22 | self._name = "AutoGPT-YouTube" 23 | self._version = "0.3.0" 24 | self._description = "This is a plugin for Auto-GPT which enables access to certain YouTube features." 25 | 26 | # Get the YouTube API key from the env. if it does not exist give a warning 27 | self.yt_api_key = os.environ.get("YOUTUBE_API_KEY") 28 | if self.yt_api_key is None: 29 | print( 30 | "WARNING: The YouTube API key is not set, therefore API commands are disabled. Please set the YOUTUBE_API_KEY=your_api_key environment variable." 31 | ) 32 | 33 | self.workspace_path = "autogpt\\auto_gpt_workspace" 34 | 35 | 36 | 37 | 38 | def can_handle_post_prompt(self) -> bool: 39 | """This method is called to check that the plugin can 40 | handle the post_prompt method. 41 | 42 | Returns: 43 | bool: True if the plugin can handle the post_prompt method.""" 44 | return True 45 | 46 | def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator: 47 | """This method is called just after the generate_prompt is called, 48 | but actually before the prompt is generated. 49 | 50 | Args: 51 | prompt (PromptGenerator): The prompt generator. 52 | 53 | Returns: 54 | PromptGenerator: The prompt generator. 55 | """ 56 | 57 | # non-API commands 58 | 59 | 60 | # youtube downloads 61 | from .youtube_download import download_youtube_video, download_youtube_audio 62 | prompt.add_command( 63 | "download_youtube_video", 64 | "Download a YouTube video", 65 | {"url": "