2024-02-15 05:21:20 +00:00
|
|
|
import re
|
|
|
|
from googleapiclient.discovery import build
|
|
|
|
from googleapiclient.errors import HttpError
|
|
|
|
from youtube_transcript_api import YouTubeTranscriptApi
|
|
|
|
from dotenv import load_dotenv
|
2024-05-02 14:29:14 +00:00
|
|
|
from datetime import datetime
|
2024-02-15 05:21:20 +00:00
|
|
|
import os
|
|
|
|
import json
|
|
|
|
import isodate
|
|
|
|
import argparse
|
2024-03-16 02:18:24 +00:00
|
|
|
import sys
|
2024-02-15 05:21:20 +00:00
|
|
|
|
2024-03-03 15:57:49 +00:00
|
|
|
|
2024-02-15 05:21:20 +00:00
|
|
|
def get_video_id(url):
|
|
|
|
# Extract video ID from URL
|
2024-03-13 21:15:14 +00:00
|
|
|
pattern = r"(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})"
|
2024-02-15 05:21:20 +00:00
|
|
|
match = re.search(pattern, url)
|
|
|
|
return match.group(1) if match else None
|
|
|
|
|
2024-03-03 15:57:49 +00:00
|
|
|
|
2024-03-17 06:18:04 +00:00
|
|
|
def get_comments(youtube, video_id):
|
|
|
|
comments = []
|
2024-03-17 07:29:56 +00:00
|
|
|
|
2024-03-17 06:18:04 +00:00
|
|
|
try:
|
2024-03-17 07:29:56 +00:00
|
|
|
# Fetch top-level comments
|
|
|
|
request = youtube.commentThreads().list(
|
|
|
|
part="snippet,replies",
|
2024-03-17 06:18:04 +00:00
|
|
|
videoId=video_id,
|
|
|
|
textFormat="plainText",
|
|
|
|
maxResults=100 # Adjust based on needs
|
2024-03-17 07:29:56 +00:00
|
|
|
)
|
2024-03-17 06:18:04 +00:00
|
|
|
|
2024-03-17 07:29:56 +00:00
|
|
|
while request:
|
|
|
|
response = request.execute()
|
2024-03-17 06:18:04 +00:00
|
|
|
for item in response['items']:
|
2024-03-17 07:29:56 +00:00
|
|
|
# Top-level comment
|
|
|
|
topLevelComment = item['snippet']['topLevelComment']['snippet']['textDisplay']
|
|
|
|
comments.append(topLevelComment)
|
|
|
|
|
|
|
|
# Check if there are replies in the thread
|
|
|
|
if 'replies' in item:
|
|
|
|
for reply in item['replies']['comments']:
|
|
|
|
replyText = reply['snippet']['textDisplay']
|
|
|
|
# Add incremental spacing and a dash for replies
|
|
|
|
comments.append(" - " + replyText)
|
|
|
|
|
|
|
|
# Prepare the next page of comments, if available
|
2024-03-17 06:18:04 +00:00
|
|
|
if 'nextPageToken' in response:
|
2024-03-17 07:29:56 +00:00
|
|
|
request = youtube.commentThreads().list_next(
|
|
|
|
previous_request=request, previous_response=response)
|
2024-03-17 06:18:04 +00:00
|
|
|
else:
|
2024-03-17 07:29:56 +00:00
|
|
|
request = None
|
|
|
|
|
2024-03-17 06:18:04 +00:00
|
|
|
except HttpError as e:
|
|
|
|
print(f"Failed to fetch comments: {e}")
|
2024-03-17 07:29:56 +00:00
|
|
|
|
2024-03-17 06:18:04 +00:00
|
|
|
return comments
|
|
|
|
|
|
|
|
|
2024-03-17 07:29:56 +00:00
|
|
|
|
2024-03-03 19:09:02 +00:00
|
|
|
def main_function(url, options):
|
2024-02-15 05:21:20 +00:00
|
|
|
# Load environment variables from .env file
|
2024-03-13 21:15:14 +00:00
|
|
|
load_dotenv(os.path.expanduser("~/.config/fabric/.env"))
|
2024-02-15 05:21:20 +00:00
|
|
|
|
|
|
|
# Get YouTube API key from environment variable
|
2024-03-13 21:15:14 +00:00
|
|
|
api_key = os.getenv("YOUTUBE_API_KEY")
|
2024-02-15 05:21:20 +00:00
|
|
|
if not api_key:
|
|
|
|
print("Error: YOUTUBE_API_KEY not found in ~/.config/fabric/.env")
|
|
|
|
return
|
|
|
|
|
|
|
|
# Extract video ID from URL
|
|
|
|
video_id = get_video_id(url)
|
|
|
|
if not video_id:
|
|
|
|
print("Invalid YouTube URL")
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Initialize the YouTube API client
|
2024-03-13 21:15:14 +00:00
|
|
|
youtube = build("youtube", "v3", developerKey=api_key)
|
2024-02-15 05:21:20 +00:00
|
|
|
|
|
|
|
# Get video details
|
2024-03-17 06:18:04 +00:00
|
|
|
video_response = youtube.videos().list(
|
2024-05-02 14:29:14 +00:00
|
|
|
id=video_id, part="contentDetails,snippet").execute()
|
2024-02-15 05:21:20 +00:00
|
|
|
|
|
|
|
# Extract video duration and convert to minutes
|
2024-03-13 21:15:14 +00:00
|
|
|
duration_iso = video_response["items"][0]["contentDetails"]["duration"]
|
2024-02-15 05:21:20 +00:00
|
|
|
duration_seconds = isodate.parse_duration(duration_iso).total_seconds()
|
|
|
|
duration_minutes = round(duration_seconds / 60)
|
2024-05-02 14:29:14 +00:00
|
|
|
# Set up metadata
|
|
|
|
metadata = {}
|
|
|
|
metadata['id'] = video_response['items'][0]['id']
|
|
|
|
metadata['title'] = video_response['items'][0]['snippet']['title']
|
|
|
|
metadata['channel'] = video_response['items'][0]['snippet']['channelTitle']
|
|
|
|
metadata['published_at'] = video_response['items'][0]['snippet']['publishedAt']
|
2024-02-15 05:21:20 +00:00
|
|
|
|
|
|
|
# Get video transcript
|
|
|
|
try:
|
2024-03-27 12:20:11 +00:00
|
|
|
transcript_list = YouTubeTranscriptApi.get_transcript(video_id, languages=[options.lang])
|
2024-03-17 06:18:04 +00:00
|
|
|
transcript_text = " ".join([item["text"] for item in transcript_list])
|
2024-03-13 21:15:14 +00:00
|
|
|
transcript_text = transcript_text.replace("\n", " ")
|
2024-02-15 05:21:20 +00:00
|
|
|
except Exception as e:
|
2024-03-27 12:20:11 +00:00
|
|
|
transcript_text = f"Transcript not available in the selected language ({options.lang}). ({e})"
|
2024-02-15 05:21:20 +00:00
|
|
|
|
2024-03-17 06:18:04 +00:00
|
|
|
# Get comments if the flag is set
|
|
|
|
comments = []
|
|
|
|
if options.comments:
|
|
|
|
comments = get_comments(youtube, video_id)
|
|
|
|
|
2024-02-15 05:21:20 +00:00
|
|
|
# Output based on options
|
|
|
|
if options.duration:
|
|
|
|
print(duration_minutes)
|
|
|
|
elif options.transcript:
|
2024-03-25 10:00:34 +00:00
|
|
|
print(transcript_text.encode('utf-8').decode('unicode-escape'))
|
2024-03-17 06:18:04 +00:00
|
|
|
elif options.comments:
|
|
|
|
print(json.dumps(comments, indent=2))
|
2024-05-02 14:29:14 +00:00
|
|
|
elif options.metadata:
|
|
|
|
print(json.dumps(metadata, indent=2))
|
2024-02-15 05:21:20 +00:00
|
|
|
else:
|
2024-03-17 06:18:04 +00:00
|
|
|
# Create JSON object with all data
|
|
|
|
output = {
|
|
|
|
"transcript": transcript_text,
|
|
|
|
"duration": duration_minutes,
|
2024-05-02 14:29:14 +00:00
|
|
|
"comments": comments,
|
|
|
|
"metadata": metadata
|
2024-03-17 06:18:04 +00:00
|
|
|
}
|
2024-02-15 05:21:20 +00:00
|
|
|
# Print JSON object
|
2024-03-17 06:18:04 +00:00
|
|
|
print(json.dumps(output, indent=2))
|
2024-02-15 05:21:20 +00:00
|
|
|
except HttpError as e:
|
2024-03-17 06:18:04 +00:00
|
|
|
print(f"Error: Failed to access YouTube API. Please check your YOUTUBE_API_KEY and ensure it is valid: {e}")
|
2024-03-03 15:57:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
parser = argparse.ArgumentParser(
|
2024-03-17 06:18:04 +00:00
|
|
|
description='yt (video meta) extracts metadata about a video, such as the transcript, the video\'s duration, and now comments. By Daniel Miessler.')
|
2024-03-14 18:29:56 +00:00
|
|
|
parser.add_argument('url', help='YouTube video URL')
|
2024-03-17 06:18:04 +00:00
|
|
|
parser.add_argument('--duration', action='store_true', help='Output only the duration')
|
|
|
|
parser.add_argument('--transcript', action='store_true', help='Output only the transcript')
|
|
|
|
parser.add_argument('--comments', action='store_true', help='Output the comments on the video')
|
2024-05-02 14:29:14 +00:00
|
|
|
parser.add_argument('--metadata', action='store_true', help='Output the video metadata')
|
2024-03-27 12:20:11 +00:00
|
|
|
parser.add_argument('--lang', default='en', help='Language for the transcript (default: English)')
|
|
|
|
|
2024-03-14 18:15:59 +00:00
|
|
|
args = parser.parse_args()
|
2024-03-16 02:18:24 +00:00
|
|
|
|
|
|
|
if args.url is None:
|
2024-03-17 06:18:04 +00:00
|
|
|
print("Error: No URL provided.")
|
|
|
|
return
|
2024-03-16 02:18:24 +00:00
|
|
|
|
2024-03-14 18:15:59 +00:00
|
|
|
main_function(args.url, args)
|
2024-03-17 06:18:04 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|