Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import os
- import json
- import re
- from datetime import datetime
- import pytz
- """
- This script transforms yt-dlp '.info.json' files (containing video metadata and comments) into interactive HTML webpages that simulate a YouTube video page
- without playback functionality. It extracts and displays video details, comments, and additional metadata in a structured format, including a header with a
- "FakeTube" logo, video URL, and a link to the Python script. The script processes all '.info.json' files in a specified directory which it asks for after running,
- generating a corresponding HTML file for each to simulate the video page with functional elements like comment sorting and expandable descriptions.
- ### Key Features / Benefits:
- - **Unique Comment Sorting**: Allows sorting comments by Most Likes, Most Replies, Longest Length, or Alphabetically, offering insights not readily available on YouTube.
- - **Simulated YouTube Page**: Ideal for viewing a realistic, interactive version of a video's page, including metadata like tags, categories, and video statistics.
- - **Proactive Archiving**: Useful for archiving channels or videos that may be terminated, as it simulates the video page without relying on external services like The Wayback Machine.
- - Superior to The Wayback Machine: Archives comments correctly and provides sorting options, unlike snapshots which lack full functionality or miss comments/videos themselves.
- - Can be generated for any video without downloading the video itself, ensuring comprehensive archiving.
- ### Requirements:
- - Python terminal, like IDLE.
- - Python libraries: 'os', 'json', 're', 'datetime', and 'pytz'. (json must be installed manually; it's easy to install packaged using Gitbash and "pip install json").
- - '.info.json' files created by yt-dlp, containing video metadata and comments, in a folder.
- ### Process:
- 1. Run the script, and paste your full directory with the json(s) you want to run it on. Full directory can be copied by clicking the windows' file explorer address bar.
- 2. For each '.info.json' file:
- - Extracts video metadata (e.g., title, uploader, view count, likes, description, etc.).
- - Processes comments, including sorting options and reply threading.
- - Generates an HTML file with:
- - A header featuring the "FakeTube" logo, video URL (clickable and copyable), and a script link.
- - A simulated video player with the video thumbnail.
- - Video details, including uploader information, like/dislike bar, and a collapsible description.
- - A comment section with sorting options and expandable replies.
- - Additional metadata like tags, categories, and video file details.
- 3. Saves each HTML file in the same directory with the base filename and a '.html' extension.
- ### Usage:
- 1. Install prerequisites.
- 2. Run the script. It will process all '.info.json' files in the specified directory of your choosing, and generate corresponding HTML files.
- 3. Open the generated HTML files in a web browser to view the simulated YouTube video page.
- ### Customization:
- - Modify the HTML and CSS in the script to alter the webpage's appearance (e.g., change colors, adjust layout, add new buttons, or modify the comment sorting options).
- - Extend the script to include additional metadata or features by parsing more fields from the '.info.json' files (most is already included).
- - Update the `pastebin_link` variable to point to your own script's source or documentation.
- ### Limitations:
- - Comments and replies cannot be sorted by Newest due to limitations in timestamp precision, but the script offers unique sorting methods.
- - Parent comments can't be sorted by newest/oldest, and neither can replies (they will sort by most likes instead).
- - The uploader's profile picture is extracted from comments if available; otherwise, a placeholder is used.
- - yt-dlp jsons don't appear to save the uploader's profile picture by default. HTML files can't easily access local files, like thumbnails, either, from what I've tried testing.
- - The script is designed to work with '.info.json' files; if your files have a different naming convention, adjust the filename filter in the script.
- - Pinned comments display as pinned, but don't stay at the top of the section. This should be possible to code, but I decided to exclude it for accuracy in comment sorting.
- - Timestamps on videos, representing when they were scheduled to release, are in UTC as a universal reference. Convert to your timezone for your usage.
- """
- def get_directory():
- while True:
- path = input("Please enter the directory path containing .info.json files: ").strip()
- if os.path.isdir(path):
- return path
- else:
- print(f"Error: '{path}' is not a directory. Please try again.")
- # Set the directory by prompting the user
- directory = get_directory()
- # Function to convert timestamp to readable date and time
- def timestamp_to_readable(ts):
- if ts and isinstance(ts, (int, float)):
- dt = datetime.fromtimestamp(ts, tz=pytz.UTC)
- return dt.strftime("%m/%d/%Y %H:%M:%S UTC")
- return "N/A"
- # Function to format numbers (e.g., subscriber count, views)
- def format_number(num):
- if num is None:
- return "N/A"
- if isinstance(num, (int, float)):
- return "{:,}".format(int(num))
- return str(num)
- # Function to extract YouTube ID from filename
- def extract_youtube_id(filename):
- match = re.search(r'\[([a-zA-Z0-9_-]{11})\]', filename)
- return match.group(1) if match else None
- # Function to get uploader's profile picture from comments
- def get_uploader_pfp(comments):
- for comment in comments:
- if comment.get("author_is_uploader", False):
- return comment.get("author_thumbnail", "https://via.placeholder.com/48?text=")
- return "https://via.placeholder.com/48?text=" # Black circle if not found
- # Pastebin link placeholder
- pastebin_link = "https://pastebin.com/your_link_here" # Pastebin link for this script
- # Process each .info.json file
- for filename in os.listdir(directory):
- if filename.endswith(".info.json"): #REMOVE ".info" IF YOU WANT
- with open(os.path.join(directory, filename), 'r', encoding='utf-8') as f:
- data = json.load(f)
- mod_time = os.path.getmtime(os.path.join(directory, filename))
- mod_dt = datetime.fromtimestamp(mod_time, tz=pytz.UTC)
- month = mod_dt.month
- mod_str = f"{month}{mod_dt.strftime('/%d/%Y %I:%M:%S %p')}"
- # Extract video ID
- video_id = data.get("id", data.get("display_id", extract_youtube_id(filename)))
- if not video_id:
- continue
- video_url = f"https://www.youtube.com/watch?v={video_id}"
- thumbnail_url = data.get("thumbnail", "https://via.placeholder.com/640x360?text=Thumbnail+Not+Found")
- # Video metadata retrieval
- title = data.get("title", "N/A")
- channel_name = data.get("uploader", "N/A")
- channel_id = data.get("channel_id", "N/A")
- channel_url = data.get("channel_url", f"https://www.youtube.com/channel/{data.get('channel_id', 'N/A')}")
- uploader_url = data.get("uploader_url", channel_url)
- channel_pfp = get_uploader_pfp(data.get("comments", []))
- subscriber_count = format_number(data.get("channel_follower_count"))
- view_count = format_number(data.get("view_count"))
- upload_date = data.get("upload_date", "N/A")
- if upload_date != "N/A" and len(upload_date) == 8:
- upload_date = f"{upload_date[4:6]}/{upload_date[6:8]}/{upload_date[0:4]}"
- timestamp = timestamp_to_readable(data.get("timestamp"))
- timestamp_display = f" • Scheduled: {timestamp}" if timestamp != "N/A" else ""
- # Convert URLs in description to clickable links
- description = re.sub(r'(https?://[^\s]+)', r'<a href="\1" target="_blank">\1</a>', data.get("description", "N/A")).replace("\n", "<br>")
- likes = format_number(data.get("like_count", 0))
- dislikes = format_number(data.get("dislike_count")) if "dislike_count" in data else None
- age_limit = data.get("age_limit", 0)
- comments = data.get("comments", [])
- tags = ", ".join(data.get("tags", [])) if data.get("tags") else "N/A"
- duration_string = data.get("duration_string", "N/A")
- duration = data.get("duration", "N/A")
- availability = data.get("availability", "N/A")
- resolution = data.get("resolution", "N/A")
- fps = data.get("fps", "N/A")
- vcodec = data.get("vcodec", "N/A")
- acodec = data.get("acodec", "N/A")
- fulltitle = data.get("fulltitle", title)
- categories = ", ".join(data.get("categories", [])) if data.get("categories") else "N/A"
- dynamic_range = data.get("dynamic_range", "N/A")
- tbr = data.get("tbr", "N/A")
- vbr = data.get("vbr", "N/A")
- abr = data.get("abr", "N/A")
- asr = data.get("asr", "N/A")
- filesize_approx = format_number(data.get("filesize_approx"))
- format_note = data.get("format_note", "N/A")
- format_id = data.get("format_id", "N/A")
- format_ = data.get("format", "N/A")
- video_ext = data.get("video_ext", "N/A")
- ext = data.get("ext", "N/A")
- container = data.get("container", "N/A")
- channel_handle = channel_name if channel_name.startswith('@') else f"@{data.get('uploader_id', 'N/A')}"
- # Handle comments with replies and sorting
- if not comments:
- comment_section = "<h3>No YouTube Comments Saved</h3>"
- else:
- comment_html = {"likes": [], "alpha": []}
- root_comments = [c for c in comments if c.get("parent", "root") == "root"]
- replies_dict = {}
- for comment in comments:
- parent_id = comment.get("parent", "root")
- if parent_id != "root":
- if parent_id not in replies_dict:
- replies_dict[parent_id] = []
- replies_dict[parent_id].append(comment)
- for comment in root_comments:
- # Determine the text and clickable link for the comment author.
- author_text = comment.get("author", "N/A")
- if author_text.startswith("@"):
- clickable_author_link = f"https://www.youtube.com/{author_text}"
- else:
- clickable_author_link = f"https://www.youtube.com/channel/{comment.get('author_id', 'N/A')}"
- # The Copy Channel URL button always uses the channel URL based on author_id.
- copy_channel_url = f"https://www.youtube.com/channel/{comment.get('author_id', 'N/A')}"
- author_pfp = comment.get("author_thumbnail", "https://via.placeholder.com/48?text=")
- text = comment.get("text", "").replace("\n", "<br>")
- text = re.sub(r'(@[a-zA-Z0-9_-]+)', r'<a href="https://www.youtube.com/\1" target="_blank">\1</a>', text)
- likes_count = format_number(comment.get("like_count", 0))
- time_text = comment.get("_time_text", comment.get("time_text", "N/A"))
- is_uploader = comment.get("author_is_uploader", False)
- is_verified = comment.get("author_is_verified", False)
- is_pinned = comment.get("is_pinned", False)
- comment_id = comment.get("id", "")
- author_class = "comment-author-uploader" if is_uploader else "comment-author"
- verified_check = "✔" if is_verified else ""
- pinned_note = "<span class='pinned'>📌 Pinned 📌</span>" if is_pinned else ""
- # Handle replies
- replies = replies_dict.get(comment_id, [])
- replies_html = ""
- if replies:
- sorted_replies = sorted(replies, key=lambda x: int(x.get("like_count", 0) or 0), reverse=True)
- replies_html = '<div class="replies" style="display: none;">'
- for reply in sorted_replies:
- r_author_text = reply.get("author", "N/A")
- if r_author_text.startswith("@"):
- r_clickable_author_link = f"https://www.youtube.com/{r_author_text}"
- else:
- r_clickable_author_link = f"https://www.youtube.com/channel/{reply.get('author_id', 'N/A')}"
- r_copy_channel_url = f"https://www.youtube.com/channel/{reply.get('author_id', 'N/A')}"
- r_author_pfp = reply.get("author_thumbnail", "https://via.placeholder.com/48?text=")
- r_text = reply.get("text", "").replace("\n", "<br>")
- r_text = re.sub(r'(@[a-zA-Z0-9_-]+)', r'<a href="https://www.youtube.com/\1" target="_blank">\1</a>', r_text)
- r_likes = format_number(reply.get("like_count", 0))
- r_time = reply.get("_time_text", reply.get("time_text", "N/A"))
- replies_html += f"""
- <div class="comment reply">
- <a href="{r_author_pfp}" target="_blank" onclick="event.preventDefault(); window.open('{r_author_pfp}', '_blank');" class="comment-pfp">
- <img src="{r_author_pfp}" alt="Profile">
- </a>
- <div class="comment-content">
- <div class="comment-header">
- <a href="{r_clickable_author_link}" target="_blank" class="comment-author">{r_author_text}</a>
- <span class="comment-time">{r_time}</span>
- <button class="copy-url" onclick="navigator.clipboard.writeText('{r_copy_channel_url}')">Copy Channel URL</button>
- </div>
- <p>{r_text}</p>
- <div class="comment-likes">👍 {r_likes} {f' <span class="favorited-heart">♥<span class="tooltip">Loved by {channel_name}</span></span>' if reply.get('is_favorited', False) else ''}</div>
- </div>
- </div>
- """
- replies_html += '</div>'
- replies_html = f'<span class="reply-toggle" onclick="toggleReplies(this)" data-replies="{len(replies)}">▼ {len(replies)} repl{"y" if len(replies) == 1 else "ies"}</span>' + replies_html
- base_comment = f"""
- <div class="comment">
- <a href="{author_pfp}" target="_blank" onclick="event.preventDefault(); window.open('{author_pfp}', '_blank');" class="comment-pfp">
- <img src="{author_pfp}" alt="Profile">
- </a>
- <div class="comment-content">
- <div class="comment-header">
- <a href="{clickable_author_link}" target="_blank" class="{author_class}">{author_text} {verified_check}</a>
- <span class="comment-time">{time_text}</span>
- <button class="copy-url" onclick="navigator.clipboard.writeText('{copy_channel_url}')">Copy Channel URL</button>
- </div>
- <p>{text}</p>
- <div class="comment-likes">👍 {likes_count} {f' <span class="favorited-heart">♥<span class="tooltip">Loved by {channel_name}</span></span>' if comment.get('is_favorited', False) else ''}</div>
- {replies_html}
- </div>
- </div>
- """
- comment_html["likes"].append((int(comment.get("like_count", 0) or 0), base_comment))
- comment_html["alpha"].append((text.lower(), base_comment))
- # Sort root comments
- comment_html["likes"].sort(reverse=True) # Most liked
- comment_html["alpha"].sort() # Alphabetically
- comment_section = f"""
- <div class="comments-sorting-header">
- <h3>{len(root_comments)} Comments</h3>
- <div class="sort-container">
- <select onchange="sortComments(this.value)" class="sort-dropdown">
- <option value="likes">Most Likes</option>
- <option value="replies">Most Replies</option>
- <option value="length">Longest Length</option>
- <option value="alpha">Alphabetically</option>
- </select>
- </div>
- </div>
- <div id="comments-container">{"".join([c[1] for c in comment_html['likes']])}</div>
- """
- # Like bar rendering with tooltip
- like_bar = ''
- if dislikes and int(data.get("like_count", 0) or 0) > 0:
- like_count = int(data.get("like_count", 0))
- dislike_count = int(dislikes.replace(",", ""))
- total = like_count + dislike_count
- like_percentage = (like_count / total * 100) if total > 0 else 0
- like_bar = f'''
- <div class="like-bar" style="background: linear-gradient(to right, green {like_percentage}%, red {like_percentage}%); width: 100%;">
- <span class="tooltip">{likes} / {dislikes} - {like_percentage:.1f}%</span>
- </div>'''
- dislike_display = f'<span>👎 {dislikes}</span>' if dislikes else ''
- # HTML content
- html_content = f"""
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>{title}</title>
- <style>
- body {{
- font-family: 'Roboto', Arial, sans-serif;
- margin: 0;
- padding: 0;
- background: #f9f9f9;
- }}
- .container {{
- display: flex;
- width: 100%;
- max-width: 1600px;
- margin: 0 auto;
- }}
- .main-content {{
- width: 70%;
- padding: 10px;
- }}
- .sidebar {{
- width: 30%;
- background: white;
- display: flex;
- justify-content: center;
- align-items: flex-start;
- }}
- .header {{
- padding: 10px;
- background: #fff;
- border-bottom: 1px solid #ddd;
- }}
- .header-wrapper {{
- width: 100%;
- max-width: 1600px;
- margin: 0 auto;
- display: flex;
- }}
- .header-left {{
- width: 70%;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }}
- .header-right {{
- width: 30%;
- display: flex;
- justify-content: center;
- align-items: center;
- }}
- .logo {{
- font-size: 24px;
- margin-bottom: 3px;
- }}
- .logo span.black {{
- color: black;
- }}
- .logo span.red {{
- color: white;
- background: #cc0000;
- padding: 0 5px;
- border-radius: 5px;
- }}
- .subtitle {{
- font-size: 9px;
- }}
- .url-bar {{
- text-align: center;
- border: 1px solid #ddd;
- border-radius: 20px;
- padding: 5px 10px;
- background: #fff;
- }}
- .url-bar button {{
- border-radius: 10px;
- border: 1px solid;
- border-color: #ccc;
- }}
- .url-bar button:hover {{
- background: #e0e0e0;
- transition: background 0.2s ease;
- }}
- .open-new-tab {{
- border: 1px solid #ccc;
- border-radius: 4px;
- background-color: #f8f8f8;
- cursor: pointer;
- }}
- .open-new-tab:hover {{
- background-color: #e8e8e8;
- transition: background-color 0.2s ease;
- }}
- .video-player {{
- width: 100%;
- height: 550px;
- background: black;
- margin: 0 0 20px 0;
- border-radius: 10px;
- overflow: hidden;
- }}
- .video-player img {{
- width: 100%;
- height: 100%;
- object-fit: cover;
- }}
- .video-title {{
- font-size: 18px;
- font-weight: bold;
- margin: 10px 0;
- }}
- .channel-info {{
- display: flex;
- align-items: center;
- margin-bottom: 10px;
- }}
- .channel-pfp {{
- width: 48px;
- height: 48px;
- border-radius: 50%;
- margin-right: 10px;
- }}
- .channel-name {{
- font-size: 15px;
- font-weight: bold;
- margin-bottom: 3px;
- }}
- .channel-name a {{
- text-decoration: none;
- color: black;
- }}
- .subscriber-count {{
- font-size: 12px;
- color: #606060;
- }}
- .verified {{
- margin-left: 5px;
- font-size: 12px;
- }}
- .like-container {{
- margin-left: auto;
- display: flex;
- align-items: flex-start;
- gap: 4px;
- }}
- .like-subcontainer {{
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- }}
- .like-box {{
- background: #e0e0e0;
- border-radius: 20px;
- padding: 5px 10px;
- display: inline-flex;
- align-items: center;
- }}
- .like-bar-container {{
- width: 143px;
- margin-top: 5px;
- position: relative;
- }}
- .like-bar {{
- height: 3px;
- border-radius: 2px;
- width: 100%;
- }}
- .copy-info-box {{
- background: #e0e0e0;
- border-radius: 20px;
- padding: 5px 10px;
- margin-left: 3px;
- }}
- .copy-info-box .dropdown button {{
- border-radius: 20px;
- border-color: #ccc;
- }}
- .description {{
- background: #f2f2f2;
- padding: 15px;
- border-radius: 8px;
- margin: 20px 0;
- position: relative;
- }}
- .description.collapsed {{
- max-height: 100px;
- overflow: hidden;
- cursor: pointer;
- }}
- .description .toggle {{
- position: absolute;
- bottom: 5px;
- right: 10px;
- font-size: 14px;
- font-weight: bold;
- color: #606060;
- cursor: pointer;
- }}
- .comment {{
- display: flex;
- margin: 10px 0;
- position: relative;
- }}
- .comment.reply {{
- margin-left: 40px;
- }}
- .comment-pfp img {{
- width: 40px;
- height: 40px;
- border-radius: 50%;
- margin-right: 10px;
- }}
- .comment-content {{
- flex-grow: 1;
- }}
- .comment-header {{
- display: flex;
- align-items: center;
- margin-bottom: 10px;
- }}
- .comment-header h3 {{
- top-margin: 20px;
- bottom-margin: 20px;
- }}
- .comments-sorting-header {{
- display: flex;
- align-items: center;
- justify-content: flex-start;
- margin-bottom: 10px;
- }}
- .comment-author {{
- font-weight: 500;
- text-decoration: none;
- color: black;
- margin-right: 3px;
- }}
- .comment-author-uploader {{
- background: #ddd;
- padding: 2px 5px;
- border-radius: 10px;
- text-decoration: none;
- color: black;
- }}
- .likes-container {{
- display: inline-flex;
- align-items: center;
- gap: 5px;
- margin: 20px 0;
- }}
- .favorited-heart {{
- color: red;
- font-size: 20px;
- }}
- .copy-url {{
- margin-left: auto;
- font-size: 10px;
- border-radius: 9px;
- border-color: #ccc;
- }}
- .copy-url:hover {{
- background: #e8e8e8;
- transition: background 0.2s ease;
- cursor: pointer;
- }}
- .comment-time {{
- font-size: 12px;
- color: #606060;
- margin-left: 3px;
- }}
- .comment-likes {{
- margin: 20px 0;
- }}
- .pinned {{
- color: black;
- font-size: 16px;
- margin-left: 5px;
- text-decoration: none;
- }}
- .reply-toggle {{
- color: blue;
- cursor: pointer;
- margin-top: 15px;
- margin-bottom: 30px;
- display: block;
- padding: 4px 4px;
- border-radius: 6px;
- transition: background-color 0.2s ease;
- }}
- .reply-toggle:hover {{
- background-color: #e8f0fe;
- }}
- .sort-container {{
- margin-left: 10px;
- }}
- .sort-dropdown {{
- background: #e0e0e0;
- border: none;
- padding: 5px;
- border-radius: 5px;
- cursor: pointer;
- }}
- .additional-info {{
- background: #e0e0e0;
- padding: 3px 15px 15px 15px;
- border-radius: 8px;
- margin-top: 20px;
- }}
- .dropdown {{
- position: relative;
- display: inline-block;
- transition: 0.2s ease;
- }}
- .dropdown-content {{
- display: none;
- position: absolute;
- background: white;
- box-shadow: 0px 8px 16px rgba(0,0,0,0.2);
- z-index: 1;
- transition: background 0.2s ease;
- }}
- .dropdown:hover .dropdown-content {{
- display: block;
- transition: 0.2s ease;
- }}
- .dropdown-content button {{
- display: block;
- width: 100%;
- text-align: center;
- padding: 10px;
- border: none;
- background: none;
- cursor: pointer;
- }}
- .dropdown-content button:hover {{
- background: #f1f1f1;
- }}
- .script-link {{
- border: 1px solid #ddd;
- border-radius: 20px;
- padding: 5px 10px;
- text-decoration: none;
- color: black;
- transition: background 0.2s ease;
- }}
- .script-link:hover {{
- background: #e0e0e0;
- }}
- .tutorial-links {{
- text-align: center;
- margin-top: 10px;
- }}
- .tutorial-placeholder {{
- color: #0000EE;
- text-decoration: underline;
- }}
- .tutorial-links-container {{
- border: 1px solid #ddd;
- border-radius: 20px;
- padding: 10px;
- margin: 10px;
- display: inline-block;
- align-items: center;
- }}
- .file-modified {{
- margin-top: auto;
- border: 1px solid #ccc;
- border-radius: 15px;
- padding: 10px;
- text-align: center;
- font-size: 12px;
- background: #f2f2f2;
- }}
- .script-link-wrapper {{
- width: 30%;
- display: flex;
- justify-content: center;
- }}
- .tooltip {{
- position: absolute;
- background-color: rgba(0,0,0,0.8);
- color: white;
- padding: 5px 10px;
- border-radius: 4px;
- font-size: 12px;
- white-space: nowrap;
- opacity: 0;
- visibility: hidden;
- transition: opacity 0.2s ease-in-out;
- z-index: 1000;
- top: 100%;
- left: 50%;
- transform: translateX(-50%);
- margin-bottom: 5px;
- }}
- .tooltip::after {{
- content: '';
- position: absolute;
- bottom: 100%;
- left: 50%;
- margin-left: -5px;
- border-width: 5px;
- border-style: solid;
- border-color: transparent transparent rgba(0,0,0,0.8) transparent;
- }}
- .file-modified,
- .favorited-heart,
- .like-bar {{
- position: relative;
- }}
- .file-modified:hover .tooltip,
- .favorited-heart:hover .tooltip,
- .like-bar:hover .tooltip {{
- opacity: 1;
- visibility: visible;
- }}
- </style>
- </head>
- <body>
- <div class="header">
- <div class="header-wrapper">
- <div class="header-left">
- <div class="logo-container">
- <div class="logo"><span class="black">Fake</span><span class="red">Tube</span></div>
- <div class="subtitle">Rendered with <a href="https://github.com/yt-dlp/yt-dlp" target="_blank">YT-DLP</a> .info.json files</div>
- </div>
- <div class="url-bar">
- <span onclick="navigator.clipboard.writeText('{video_url}')">{video_url}</span>
- <button class="open-new-tab" onclick="window.open('{video_url}', '_blank')">Open in New Tab</button>
- </div>
- </div>
- <div class="header-right">
- <a href="{pastebin_link}" target="_blank" class="script-link">Open Python Script in New Tab</a>
- </div>
- </div>
- </div>
- <div class="container">
- <div class="main-content">
- <div class="video-player">
- <img src="{thumbnail_url}" alt="Video Thumbnail">
- </div>
- <h1 class="video-title">{title}</h1>
- <div class="channel-info">
- <img src="{channel_pfp}" class="channel-pfp" alt="N/A">
- <div>
- <div class="channel-name"><a href="{channel_url}" target="_blank">{channel_name}</a>{' ✔' if data.get('channel_is_verified', False) else ''}</div>
- <div class="subscriber-count">{subscriber_count} subscribers</div>
- </div>
- <div class="like-container">
- <!-- Subcontainer for the like box & bar (stacked) -->
- <div class="like-subcontainer">
- <div class="like-box">
- <span>👍 {likes}</span> {dislike_display}
- </div>
- <div class="like-bar-container">
- {like_bar}
- </div>
- </div>
- <div class="copy-info-box">
- <div class="dropdown">
- <button onclick="incrementLikes()">Copy Info</button>
- <div class="dropdown-content">
- <button onclick="navigator.clipboard.writeText('{video_url}');">Video URL</button>
- <button onclick="navigator.clipboard.writeText('{video_id}');">Video ID</button>
- <button onclick="navigator.clipboard.writeText('{channel_url}');">Channel URL</button>
- <button onclick="navigator.clipboard.writeText('{channel_id}');">Channel ID</button>
- <button onclick="navigator.clipboard.writeText('{uploader_url}');">Channel Handle URL</button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="description collapsed" onclick="expandDescription(this)">
- <div style="font-weight: bold; float: left;">{view_count} views • {upload_date}{timestamp_display}</div>
- <div style="clear: both; padding-top: 5px;">{description}</div>
- <span class="toggle" onclick="event.stopPropagation(); toggleDescription(this.parentElement)">Show more</span>
- </div>
- {'<p style="font-weight: bold;">Age-restricted video</p>' if age_limit > 0 else ''}
- {comment_section}
- <div class="additional-info">
- <h3>Additional Info</h3>
- <p><strong>Full Title:</strong> {fulltitle}</p>
- <p><strong>Tags:</strong> {tags}</p>
- <p><strong>Categories:</strong> {categories}</p>
- <p><strong>Video Length:</strong> {duration_string}</p>
- <p><strong>Duration (Seconds):</strong> {duration}</p>
- <p><strong>Thumbnail URL:</strong> <a href="{thumbnail_url}" target="_blank">{thumbnail_url}</a></p>
- <p><strong>Availability:</strong> {availability}</p>
- <p> </p>
- <strong>▼ Specific to the Video File ▼</strong>
- <p> </p>
- <p><strong>Approx. Filesize (Bytes):</strong> {filesize_approx}</p>
- <p><strong>Resolution:</strong> {resolution}</p>
- <p><strong>FPS:</strong> {fps}</p>
- <p><strong>Video Codec:</strong> {vcodec}</p>
- <p><strong>Audio Codec:</strong> {acodec}</p>
- <p><strong>Format:</strong> {format_}</p>
- <p><strong>Format ID:</strong> {format_id}</p>
- <p><strong>Format Note:</strong> {format_note}</p>
- <p><strong>Video Extension:</strong> {video_ext}</p>
- <p><strong>File Extension:</strong> {ext}</p>
- <p><strong>Container:</strong> {container}</p>
- <p><strong>Dynamic Range:</strong> {dynamic_range}</p>
- <p><strong>Total Bitrate:</strong> {tbr}</p>
- <p><strong>Average Bitrate:</strong> {abr}</p>
- <p><strong>Variable Bitrate:</strong> {vbr}</p>
- <p><strong>Audio Sample Rate:</strong> {asr}</p>
- </div>
- </div>
- <div class="sidebar">
- <div class="tutorial-links-container">
- <div class="tutorial-links">
- <p><a target="_blank" href="https://docs.google.com/document/d/1nzr8p1-hBfTq3Tv98d5-yTnGEBxPUDVIcFi90s0HYvs" class="tutorial-placeholder">yt-dlp Tutorial Mega-Document</a></p>
- <p><a target="_blank" href="https://docs.google.com/document/d/1adqUaJ9jmQVirhZdLm49p5hyMr-sPDLHvWHLye_Ejog" class="tutorial-placeholder">Rapid Clipping Tutorial Document</a></p>
- </div>
- <div class="file-modified">
- <strong>JSON File Creation Date:</strong> {mod_str}
- <span class="tooltip">Based on Date Modified Value</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <script>
- function incrementLikes() {{
- let likesSpan = document.querySelector('.like-box span');
- let likes = parseInt(likesSpan.textContent.replace('👍 ', '').replace(/,/g, '')) + 1;
- likesSpan.textContent = '👍 ' + likes.toLocaleString();
- }}
- function expandDescription(element) {{
- // Clicking the big description area only expands if it's collapsed.
- if (element.classList.contains('collapsed')) {{
- element.classList.remove('collapsed');
- element.querySelector('.toggle').textContent = 'Show less';
- }}
- }}
- function toggleDescription(element) {{
- // If it's collapsed, expand it. Otherwise, collapse it.
- if (element.classList.contains('collapsed')) {{
- element.classList.remove('collapsed');
- element.querySelector('.toggle').textContent = 'Show less';
- }} else {{
- element.classList.add('collapsed');
- element.querySelector('.toggle').textContent = 'Show more';
- }}
- }}
- function toggleReplies(element) {{
- let replies = element.nextElementSibling;
- if (replies.style.display === 'none' || replies.style.display === '') {{
- replies.style.display = 'block';
- element.textContent = element.textContent.replace('▼', '▲');
- }} else {{
- replies.style.display = 'none';
- element.textContent = element.textContent.replace('▲', '▼');
- }}
- }}
- function sortComments(method) {{
- let container = document.getElementById('comments-container');
- if (!container) return;
- let comments = Array.from(container.getElementsByClassName('comment')).filter(c => !c.classList.contains('reply'));
- comments.sort((a, b) => {{
- let textA = a.querySelector('p').textContent.trim().toLowerCase();
- let textB = b.querySelector('p').textContent.trim().toLowerCase();
- if (method === 'alpha') {{
- return textA.localeCompare(textB);
- }} else if (method === 'replies') {{
- let repliesA = a.querySelector('.reply-toggle') ? parseInt(a.querySelector('.reply-toggle').dataset.replies) : 0;
- let repliesB = b.querySelector('.reply-toggle') ? parseInt(b.querySelector('.reply-toggle').dataset.replies) : 0;
- return repliesB - repliesA || textA.localeCompare(textB);
- }} else if (method === 'length') {{
- let lengthA = a.querySelector('p').textContent.length;
- let lengthB = b.querySelector('p').textContent.length;
- return lengthB - lengthA || textA.localeCompare(textB);
- }} else {{ // Most Likes
- let likesA = parseInt(a.querySelector('.comment-likes').textContent.replace('👍 ', '').replace(/,/g, '')) || 0;
- let likesB = parseInt(b.querySelector('.comment-likes').textContent.replace('👍 ', '').replace(/,/g, '')) || 0;
- return likesB - likesA || textA.localeCompare(textB);
- }}
- }});
- container.innerHTML = '';
- comments.forEach(comment => container.appendChild(comment));
- }}
- function setupDropdownReset() {{
- let dropdownButtons = document.querySelectorAll('.dropdown-content button');
- dropdownButtons.forEach(button => {{
- button.addEventListener('click', function() {{
- // Hide the dropdown temporarily
- let dropdownContent = this.parentElement;
- dropdownContent.style.display = 'none';
- // After a short delay, remove the inline style so .dropdown:hover can work again
- setTimeout(() => {{
- dropdownContent.removeAttribute('style');
- }}, 150);
- }});
- }});
- }}
- window.onload = setupDropdownReset;
- </script>
- </body>
- </html>
- """
- # Write HTML file
- output_filename = os.path.splitext(filename)[0] + ".html"
- with open(os.path.join(directory, output_filename), 'w', encoding='utf-8') as f:
- f.write(html_content)
- print("HTML files generated successfully.")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement