Advertisement
fckerfckersht

Render YouTube Comments as an HTML Webpage MkIII V8.8

Apr 9th, 2025
316
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 39.38 KB | None | 0 0
  1. import os
  2. import json
  3. import re
  4. from datetime import datetime
  5. import pytz
  6.  
  7. """
  8. This script transforms yt-dlp '.info.json' files (containing video metadata and comments) into interactive HTML webpages that simulate a YouTube video page
  9. without playback functionality. It extracts and displays video details, comments, and additional metadata in a structured format, including a header with a
  10. "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,
  11. generating a corresponding HTML file for each to simulate the video page with functional elements like comment sorting and expandable descriptions.
  12.  
  13.  
  14. ### Key Features / Benefits:
  15. - **Unique Comment Sorting**: Allows sorting comments by Most Likes, Most Replies, Longest Length, or Alphabetically, offering insights not readily available on YouTube.
  16. - **Simulated YouTube Page**: Ideal for viewing a realistic, interactive version of a video's page, including metadata like tags, categories, and video statistics.
  17. - **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.
  18. - Superior to The Wayback Machine: Archives comments correctly and provides sorting options, unlike snapshots which lack full functionality or miss comments/videos themselves.
  19. - Can be generated for any video without downloading the video itself, ensuring comprehensive archiving.
  20.  
  21.  
  22. ### Requirements:
  23. - Python terminal, like IDLE.
  24. - 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").
  25. - '.info.json' files created by yt-dlp, containing video metadata and comments, in a folder.
  26.  
  27.  
  28. ### Process:
  29. 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.
  30. 2. For each '.info.json' file:
  31. - Extracts video metadata (e.g., title, uploader, view count, likes, description, etc.).
  32. - Processes comments, including sorting options and reply threading.
  33. - Generates an HTML file with:
  34. - A header featuring the "FakeTube" logo, video URL (clickable and copyable), and a script link.
  35. - A simulated video player with the video thumbnail.
  36. - Video details, including uploader information, like/dislike bar, and a collapsible description.
  37. - A comment section with sorting options and expandable replies.
  38. - Additional metadata like tags, categories, and video file details.
  39. 3. Saves each HTML file in the same directory with the base filename and a '.html' extension.
  40.  
  41.  
  42. ### Usage:
  43. 1. Install prerequisites.
  44. 2. Run the script. It will process all '.info.json' files in the specified directory of your choosing, and generate corresponding HTML files.
  45. 3. Open the generated HTML files in a web browser to view the simulated YouTube video page.
  46.  
  47.  
  48. ### Customization:
  49. - 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).
  50. - Extend the script to include additional metadata or features by parsing more fields from the '.info.json' files (most is already included).
  51. - Update the `pastebin_link` variable to point to your own script's source or documentation.
  52.  
  53.  
  54. ### Limitations:
  55. - Comments and replies cannot be sorted by Newest due to limitations in timestamp precision, but the script offers unique sorting methods.
  56. - Parent comments can't be sorted by newest/oldest, and neither can replies (they will sort by most likes instead).
  57.  
  58. - The uploader's profile picture is extracted from comments if available; otherwise, a placeholder is used.
  59. - 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.
  60. - 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.
  61. - 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.
  62. - Timestamps on videos, representing when they were scheduled to release, are in UTC as a universal reference. Convert to your timezone for your usage.
  63. """
  64.  
  65.  
  66. def get_directory():
  67. while True:
  68. path = input("Please enter the directory path containing .info.json files: ").strip()
  69. if os.path.isdir(path):
  70. return path
  71. else:
  72. print(f"Error: '{path}' is not a directory. Please try again.")
  73.  
  74. # Set the directory by prompting the user
  75. directory = get_directory()
  76.  
  77. # Function to convert timestamp to readable date and time
  78. def timestamp_to_readable(ts):
  79. if ts and isinstance(ts, (int, float)):
  80. dt = datetime.fromtimestamp(ts, tz=pytz.UTC)
  81. return dt.strftime("%m/%d/%Y %H:%M:%S UTC")
  82. return "N/A"
  83.  
  84. # Function to format numbers (e.g., subscriber count, views)
  85. def format_number(num):
  86. if num is None:
  87. return "N/A"
  88. if isinstance(num, (int, float)):
  89. return "{:,}".format(int(num))
  90. return str(num)
  91.  
  92. # Function to extract YouTube ID from filename
  93. def extract_youtube_id(filename):
  94. match = re.search(r'\[([a-zA-Z0-9_-]{11})\]', filename)
  95. return match.group(1) if match else None
  96.  
  97. # Function to get uploader's profile picture from comments
  98. def get_uploader_pfp(comments):
  99. for comment in comments:
  100. if comment.get("author_is_uploader", False):
  101. return comment.get("author_thumbnail", "https://via.placeholder.com/48?text=")
  102. return "https://via.placeholder.com/48?text=" # Black circle if not found
  103.  
  104. # Pastebin link placeholder
  105. pastebin_link = "https://pastebin.com/your_link_here" # Pastebin link for this script
  106.  
  107. # Process each .info.json file
  108. for filename in os.listdir(directory):
  109. if filename.endswith(".info.json"): #REMOVE ".info" IF YOU WANT
  110. with open(os.path.join(directory, filename), 'r', encoding='utf-8') as f:
  111. data = json.load(f)
  112.  
  113. mod_time = os.path.getmtime(os.path.join(directory, filename))
  114. mod_dt = datetime.fromtimestamp(mod_time, tz=pytz.UTC)
  115. month = mod_dt.month
  116. mod_str = f"{month}{mod_dt.strftime('/%d/%Y %I:%M:%S %p')}"
  117.  
  118. # Extract video ID
  119. video_id = data.get("id", data.get("display_id", extract_youtube_id(filename)))
  120. if not video_id:
  121. continue
  122.  
  123. video_url = f"https://www.youtube.com/watch?v={video_id}"
  124. thumbnail_url = data.get("thumbnail", "https://via.placeholder.com/640x360?text=Thumbnail+Not+Found")
  125.  
  126. # Video metadata retrieval
  127. title = data.get("title", "N/A")
  128. channel_name = data.get("uploader", "N/A")
  129. channel_id = data.get("channel_id", "N/A")
  130. channel_url = data.get("channel_url", f"https://www.youtube.com/channel/{data.get('channel_id', 'N/A')}")
  131. uploader_url = data.get("uploader_url", channel_url)
  132. channel_pfp = get_uploader_pfp(data.get("comments", []))
  133. subscriber_count = format_number(data.get("channel_follower_count"))
  134. view_count = format_number(data.get("view_count"))
  135. upload_date = data.get("upload_date", "N/A")
  136. if upload_date != "N/A" and len(upload_date) == 8:
  137. upload_date = f"{upload_date[4:6]}/{upload_date[6:8]}/{upload_date[0:4]}"
  138. timestamp = timestamp_to_readable(data.get("timestamp"))
  139. timestamp_display = f" • Scheduled: {timestamp}" if timestamp != "N/A" else ""
  140. # Convert URLs in description to clickable links
  141. description = re.sub(r'(https?://[^\s]+)', r'<a href="\1" target="_blank">\1</a>', data.get("description", "N/A")).replace("\n", "<br>")
  142. likes = format_number(data.get("like_count", 0))
  143. dislikes = format_number(data.get("dislike_count")) if "dislike_count" in data else None
  144. age_limit = data.get("age_limit", 0)
  145. comments = data.get("comments", [])
  146. tags = ", ".join(data.get("tags", [])) if data.get("tags") else "N/A"
  147. duration_string = data.get("duration_string", "N/A")
  148. duration = data.get("duration", "N/A")
  149. availability = data.get("availability", "N/A")
  150. resolution = data.get("resolution", "N/A")
  151. fps = data.get("fps", "N/A")
  152. vcodec = data.get("vcodec", "N/A")
  153. acodec = data.get("acodec", "N/A")
  154. fulltitle = data.get("fulltitle", title)
  155. categories = ", ".join(data.get("categories", [])) if data.get("categories") else "N/A"
  156. dynamic_range = data.get("dynamic_range", "N/A")
  157. tbr = data.get("tbr", "N/A")
  158. vbr = data.get("vbr", "N/A")
  159. abr = data.get("abr", "N/A")
  160. asr = data.get("asr", "N/A")
  161. filesize_approx = format_number(data.get("filesize_approx"))
  162. format_note = data.get("format_note", "N/A")
  163. format_id = data.get("format_id", "N/A")
  164. format_ = data.get("format", "N/A")
  165. video_ext = data.get("video_ext", "N/A")
  166. ext = data.get("ext", "N/A")
  167. container = data.get("container", "N/A")
  168. channel_handle = channel_name if channel_name.startswith('@') else f"@{data.get('uploader_id', 'N/A')}"
  169.  
  170. # Handle comments with replies and sorting
  171. if not comments:
  172. comment_section = "<h3>No YouTube Comments Saved</h3>"
  173. else:
  174. comment_html = {"likes": [], "alpha": []}
  175. root_comments = [c for c in comments if c.get("parent", "root") == "root"]
  176. replies_dict = {}
  177. for comment in comments:
  178. parent_id = comment.get("parent", "root")
  179. if parent_id != "root":
  180. if parent_id not in replies_dict:
  181. replies_dict[parent_id] = []
  182. replies_dict[parent_id].append(comment)
  183.  
  184. for comment in root_comments:
  185. # Determine the text and clickable link for the comment author.
  186. author_text = comment.get("author", "N/A")
  187. if author_text.startswith("@"):
  188. clickable_author_link = f"https://www.youtube.com/{author_text}"
  189. else:
  190. clickable_author_link = f"https://www.youtube.com/channel/{comment.get('author_id', 'N/A')}"
  191. # The Copy Channel URL button always uses the channel URL based on author_id.
  192. copy_channel_url = f"https://www.youtube.com/channel/{comment.get('author_id', 'N/A')}"
  193.  
  194. author_pfp = comment.get("author_thumbnail", "https://via.placeholder.com/48?text=")
  195. text = comment.get("text", "").replace("\n", "<br>")
  196. text = re.sub(r'(@[a-zA-Z0-9_-]+)', r'<a href="https://www.youtube.com/\1" target="_blank">\1</a>', text)
  197. likes_count = format_number(comment.get("like_count", 0))
  198. time_text = comment.get("_time_text", comment.get("time_text", "N/A"))
  199. is_uploader = comment.get("author_is_uploader", False)
  200. is_verified = comment.get("author_is_verified", False)
  201. is_pinned = comment.get("is_pinned", False)
  202. comment_id = comment.get("id", "")
  203.  
  204. author_class = "comment-author-uploader" if is_uploader else "comment-author"
  205. verified_check = "✔" if is_verified else ""
  206. pinned_note = "<span class='pinned'>📌 Pinned 📌</span>" if is_pinned else ""
  207.  
  208. # Handle replies
  209. replies = replies_dict.get(comment_id, [])
  210. replies_html = ""
  211. if replies:
  212. sorted_replies = sorted(replies, key=lambda x: int(x.get("like_count", 0) or 0), reverse=True)
  213. replies_html = '<div class="replies" style="display: none;">'
  214. for reply in sorted_replies:
  215. r_author_text = reply.get("author", "N/A")
  216. if r_author_text.startswith("@"):
  217. r_clickable_author_link = f"https://www.youtube.com/{r_author_text}"
  218. else:
  219. r_clickable_author_link = f"https://www.youtube.com/channel/{reply.get('author_id', 'N/A')}"
  220. r_copy_channel_url = f"https://www.youtube.com/channel/{reply.get('author_id', 'N/A')}"
  221. r_author_pfp = reply.get("author_thumbnail", "https://via.placeholder.com/48?text=")
  222. r_text = reply.get("text", "").replace("\n", "<br>")
  223. r_text = re.sub(r'(@[a-zA-Z0-9_-]+)', r'<a href="https://www.youtube.com/\1" target="_blank">\1</a>', r_text)
  224. r_likes = format_number(reply.get("like_count", 0))
  225. r_time = reply.get("_time_text", reply.get("time_text", "N/A"))
  226. replies_html += f"""
  227. <div class="comment reply">
  228. <a href="{r_author_pfp}" target="_blank" onclick="event.preventDefault(); window.open('{r_author_pfp}', '_blank');" class="comment-pfp">
  229. <img src="{r_author_pfp}" alt="Profile">
  230. </a>
  231. <div class="comment-content">
  232. <div class="comment-header">
  233. <a href="{r_clickable_author_link}" target="_blank" class="comment-author">{r_author_text}</a>
  234. <span class="comment-time">{r_time}</span>
  235. <button class="copy-url" onclick="navigator.clipboard.writeText('{r_copy_channel_url}')">Copy Channel URL</button>
  236. </div>
  237. <p>{r_text}</p>
  238. <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>
  239. </div>
  240. </div>
  241. """
  242. replies_html += '</div>'
  243. 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
  244.  
  245. base_comment = f"""
  246. <div class="comment">
  247. <a href="{author_pfp}" target="_blank" onclick="event.preventDefault(); window.open('{author_pfp}', '_blank');" class="comment-pfp">
  248. <img src="{author_pfp}" alt="Profile">
  249. </a>
  250. <div class="comment-content">
  251. <div class="comment-header">
  252. <a href="{clickable_author_link}" target="_blank" class="{author_class}">{author_text} {verified_check}</a>
  253. <span class="comment-time">{time_text}</span>
  254. <button class="copy-url" onclick="navigator.clipboard.writeText('{copy_channel_url}')">Copy Channel URL</button>
  255. </div>
  256. <p>{text}</p>
  257. <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>
  258. {replies_html}
  259. </div>
  260. </div>
  261. """
  262. comment_html["likes"].append((int(comment.get("like_count", 0) or 0), base_comment))
  263. comment_html["alpha"].append((text.lower(), base_comment))
  264.  
  265. # Sort root comments
  266. comment_html["likes"].sort(reverse=True) # Most liked
  267. comment_html["alpha"].sort() # Alphabetically
  268.  
  269. comment_section = f"""
  270. <div class="comments-sorting-header">
  271. <h3>{len(root_comments)} Comments</h3>
  272. <div class="sort-container">
  273. <select onchange="sortComments(this.value)" class="sort-dropdown">
  274. <option value="likes">Most Likes</option>
  275. <option value="replies">Most Replies</option>
  276. <option value="length">Longest Length</option>
  277. <option value="alpha">Alphabetically</option>
  278. </select>
  279. </div>
  280. </div>
  281. <div id="comments-container">{"".join([c[1] for c in comment_html['likes']])}</div>
  282. """
  283.  
  284. # Like bar rendering with tooltip
  285. like_bar = ''
  286. if dislikes and int(data.get("like_count", 0) or 0) > 0:
  287. like_count = int(data.get("like_count", 0))
  288. dislike_count = int(dislikes.replace(",", ""))
  289. total = like_count + dislike_count
  290. like_percentage = (like_count / total * 100) if total > 0 else 0
  291. like_bar = f'''
  292. <div class="like-bar" style="background: linear-gradient(to right, green {like_percentage}%, red {like_percentage}%); width: 100%;">
  293. <span class="tooltip">{likes} / {dislikes} - {like_percentage:.1f}%</span>
  294. </div>'''
  295. dislike_display = f'<span>👎 {dislikes}</span>' if dislikes else ''
  296.  
  297. # HTML content
  298. html_content = f"""
  299. <!DOCTYPE html>
  300. <html lang="en">
  301. <head>
  302. <meta charset="UTF-8">
  303. <title>{title}</title>
  304. <style>
  305. body {{
  306. font-family: 'Roboto', Arial, sans-serif;
  307. margin: 0;
  308. padding: 0;
  309. background: #f9f9f9;
  310. }}
  311. .container {{
  312. display: flex;
  313. width: 100%;
  314. max-width: 1600px;
  315. margin: 0 auto;
  316. }}
  317. .main-content {{
  318. width: 70%;
  319. padding: 10px;
  320. }}
  321. .sidebar {{
  322. width: 30%;
  323. background: white;
  324. display: flex;
  325. justify-content: center;
  326. align-items: flex-start;
  327. }}
  328. .header {{
  329. padding: 10px;
  330. background: #fff;
  331. border-bottom: 1px solid #ddd;
  332. }}
  333. .header-wrapper {{
  334. width: 100%;
  335. max-width: 1600px;
  336. margin: 0 auto;
  337. display: flex;
  338. }}
  339. .header-left {{
  340. width: 70%;
  341. display: flex;
  342. justify-content: space-between;
  343. align-items: center;
  344. }}
  345. .header-right {{
  346. width: 30%;
  347. display: flex;
  348. justify-content: center;
  349. align-items: center;
  350. }}
  351. .logo {{
  352. font-size: 24px;
  353. margin-bottom: 3px;
  354. }}
  355. .logo span.black {{
  356. color: black;
  357. }}
  358. .logo span.red {{
  359. color: white;
  360. background: #cc0000;
  361. padding: 0 5px;
  362. border-radius: 5px;
  363. }}
  364. .subtitle {{
  365. font-size: 9px;
  366. }}
  367. .url-bar {{
  368. text-align: center;
  369. border: 1px solid #ddd;
  370. border-radius: 20px;
  371. padding: 5px 10px;
  372. background: #fff;
  373. }}
  374. .url-bar button {{
  375. border-radius: 10px;
  376. border: 1px solid;
  377. border-color: #ccc;
  378. }}
  379. .url-bar button:hover {{
  380. background: #e0e0e0;
  381. transition: background 0.2s ease;
  382. }}
  383. .open-new-tab {{
  384. border: 1px solid #ccc;
  385. border-radius: 4px;
  386. background-color: #f8f8f8;
  387. cursor: pointer;
  388. }}
  389. .open-new-tab:hover {{
  390. background-color: #e8e8e8;
  391. transition: background-color 0.2s ease;
  392. }}
  393. .video-player {{
  394. width: 100%;
  395. height: 550px;
  396. background: black;
  397. margin: 0 0 20px 0;
  398. border-radius: 10px;
  399. overflow: hidden;
  400. }}
  401. .video-player img {{
  402. width: 100%;
  403. height: 100%;
  404. object-fit: cover;
  405. }}
  406. .video-title {{
  407. font-size: 18px;
  408. font-weight: bold;
  409. margin: 10px 0;
  410. }}
  411. .channel-info {{
  412. display: flex;
  413. align-items: center;
  414. margin-bottom: 10px;
  415. }}
  416. .channel-pfp {{
  417. width: 48px;
  418. height: 48px;
  419. border-radius: 50%;
  420. margin-right: 10px;
  421. }}
  422. .channel-name {{
  423. font-size: 15px;
  424. font-weight: bold;
  425. margin-bottom: 3px;
  426. }}
  427. .channel-name a {{
  428. text-decoration: none;
  429. color: black;
  430. }}
  431. .subscriber-count {{
  432. font-size: 12px;
  433. color: #606060;
  434. }}
  435. .verified {{
  436. margin-left: 5px;
  437. font-size: 12px;
  438. }}
  439. .like-container {{
  440. margin-left: auto;
  441. display: flex;
  442. align-items: flex-start;
  443. gap: 4px;
  444. }}
  445. .like-subcontainer {{
  446. display: flex;
  447. flex-direction: column;
  448. align-items: flex-end;
  449. }}
  450. .like-box {{
  451. background: #e0e0e0;
  452. border-radius: 20px;
  453. padding: 5px 10px;
  454. display: inline-flex;
  455. align-items: center;
  456. }}
  457. .like-bar-container {{
  458. width: 143px;
  459. margin-top: 5px;
  460. position: relative;
  461. }}
  462. .like-bar {{
  463. height: 3px;
  464. border-radius: 2px;
  465. width: 100%;
  466. }}
  467. .copy-info-box {{
  468. background: #e0e0e0;
  469. border-radius: 20px;
  470. padding: 5px 10px;
  471. margin-left: 3px;
  472. }}
  473. .copy-info-box .dropdown button {{
  474. border-radius: 20px;
  475. border-color: #ccc;
  476. }}
  477. .description {{
  478. background: #f2f2f2;
  479. padding: 15px;
  480. border-radius: 8px;
  481. margin: 20px 0;
  482. position: relative;
  483. }}
  484. .description.collapsed {{
  485. max-height: 100px;
  486. overflow: hidden;
  487. cursor: pointer;
  488. }}
  489. .description .toggle {{
  490. position: absolute;
  491. bottom: 5px;
  492. right: 10px;
  493. font-size: 14px;
  494. font-weight: bold;
  495. color: #606060;
  496. cursor: pointer;
  497. }}
  498. .comment {{
  499. display: flex;
  500. margin: 10px 0;
  501. position: relative;
  502. }}
  503. .comment.reply {{
  504. margin-left: 40px;
  505. }}
  506. .comment-pfp img {{
  507. width: 40px;
  508. height: 40px;
  509. border-radius: 50%;
  510. margin-right: 10px;
  511. }}
  512. .comment-content {{
  513. flex-grow: 1;
  514. }}
  515. .comment-header {{
  516. display: flex;
  517. align-items: center;
  518. margin-bottom: 10px;
  519. }}
  520. .comment-header h3 {{
  521. top-margin: 20px;
  522. bottom-margin: 20px;
  523. }}
  524. .comments-sorting-header {{
  525. display: flex;
  526. align-items: center;
  527. justify-content: flex-start;
  528. margin-bottom: 10px;
  529. }}
  530. .comment-author {{
  531. font-weight: 500;
  532. text-decoration: none;
  533. color: black;
  534. margin-right: 3px;
  535. }}
  536. .comment-author-uploader {{
  537. background: #ddd;
  538. padding: 2px 5px;
  539. border-radius: 10px;
  540. text-decoration: none;
  541. color: black;
  542. }}
  543. .likes-container {{
  544. display: inline-flex;
  545. align-items: center;
  546. gap: 5px;
  547. margin: 20px 0;
  548. }}
  549. .favorited-heart {{
  550. color: red;
  551. font-size: 20px;
  552. }}
  553. .copy-url {{
  554. margin-left: auto;
  555. font-size: 10px;
  556. border-radius: 9px;
  557. border-color: #ccc;
  558. }}
  559. .copy-url:hover {{
  560. background: #e8e8e8;
  561. transition: background 0.2s ease;
  562. cursor: pointer;
  563. }}
  564. .comment-time {{
  565. font-size: 12px;
  566. color: #606060;
  567. margin-left: 3px;
  568. }}
  569. .comment-likes {{
  570. margin: 20px 0;
  571. }}
  572. .pinned {{
  573. color: black;
  574. font-size: 16px;
  575. margin-left: 5px;
  576. text-decoration: none;
  577. }}
  578. .reply-toggle {{
  579. color: blue;
  580. cursor: pointer;
  581. margin-top: 15px;
  582. margin-bottom: 30px;
  583. display: block;
  584. padding: 4px 4px;
  585. border-radius: 6px;
  586. transition: background-color 0.2s ease;
  587. }}
  588. .reply-toggle:hover {{
  589. background-color: #e8f0fe;
  590. }}
  591. .sort-container {{
  592. margin-left: 10px;
  593. }}
  594. .sort-dropdown {{
  595. background: #e0e0e0;
  596. border: none;
  597. padding: 5px;
  598. border-radius: 5px;
  599. cursor: pointer;
  600. }}
  601. .additional-info {{
  602. background: #e0e0e0;
  603. padding: 3px 15px 15px 15px;
  604. border-radius: 8px;
  605. margin-top: 20px;
  606. }}
  607. .dropdown {{
  608. position: relative;
  609. display: inline-block;
  610. transition: 0.2s ease;
  611. }}
  612. .dropdown-content {{
  613. display: none;
  614. position: absolute;
  615. background: white;
  616. box-shadow: 0px 8px 16px rgba(0,0,0,0.2);
  617. z-index: 1;
  618. transition: background 0.2s ease;
  619. }}
  620. .dropdown:hover .dropdown-content {{
  621. display: block;
  622. transition: 0.2s ease;
  623. }}
  624. .dropdown-content button {{
  625. display: block;
  626. width: 100%;
  627. text-align: center;
  628. padding: 10px;
  629. border: none;
  630. background: none;
  631. cursor: pointer;
  632. }}
  633. .dropdown-content button:hover {{
  634. background: #f1f1f1;
  635. }}
  636. .script-link {{
  637. border: 1px solid #ddd;
  638. border-radius: 20px;
  639. padding: 5px 10px;
  640. text-decoration: none;
  641. color: black;
  642. transition: background 0.2s ease;
  643. }}
  644. .script-link:hover {{
  645. background: #e0e0e0;
  646. }}
  647. .tutorial-links {{
  648. text-align: center;
  649. margin-top: 10px;
  650. }}
  651. .tutorial-placeholder {{
  652. color: #0000EE;
  653. text-decoration: underline;
  654. }}
  655. .tutorial-links-container {{
  656. border: 1px solid #ddd;
  657. border-radius: 20px;
  658. padding: 10px;
  659. margin: 10px;
  660. display: inline-block;
  661. align-items: center;
  662. }}
  663. .file-modified {{
  664. margin-top: auto;
  665. border: 1px solid #ccc;
  666. border-radius: 15px;
  667. padding: 10px;
  668. text-align: center;
  669. font-size: 12px;
  670. background: #f2f2f2;
  671. }}
  672. .script-link-wrapper {{
  673. width: 30%;
  674. display: flex;
  675. justify-content: center;
  676. }}
  677. .tooltip {{
  678. position: absolute;
  679. background-color: rgba(0,0,0,0.8);
  680. color: white;
  681. padding: 5px 10px;
  682. border-radius: 4px;
  683. font-size: 12px;
  684. white-space: nowrap;
  685. opacity: 0;
  686. visibility: hidden;
  687. transition: opacity 0.2s ease-in-out;
  688. z-index: 1000;
  689. top: 100%;
  690. left: 50%;
  691. transform: translateX(-50%);
  692. margin-bottom: 5px;
  693. }}
  694. .tooltip::after {{
  695. content: '';
  696. position: absolute;
  697. bottom: 100%;
  698. left: 50%;
  699. margin-left: -5px;
  700. border-width: 5px;
  701. border-style: solid;
  702. border-color: transparent transparent rgba(0,0,0,0.8) transparent;
  703. }}
  704. .file-modified,
  705. .favorited-heart,
  706. .like-bar {{
  707. position: relative;
  708. }}
  709. .file-modified:hover .tooltip,
  710. .favorited-heart:hover .tooltip,
  711. .like-bar:hover .tooltip {{
  712. opacity: 1;
  713. visibility: visible;
  714. }}
  715. </style>
  716. </head>
  717. <body>
  718. <div class="header">
  719. <div class="header-wrapper">
  720. <div class="header-left">
  721. <div class="logo-container">
  722. <div class="logo"><span class="black">Fake</span><span class="red">Tube</span></div>
  723. <div class="subtitle">Rendered with <a href="https://github.com/yt-dlp/yt-dlp" target="_blank">YT-DLP</a> .info.json files</div>
  724. </div>
  725. <div class="url-bar">
  726. <span onclick="navigator.clipboard.writeText('{video_url}')">{video_url}</span>
  727. <button class="open-new-tab" onclick="window.open('{video_url}', '_blank')">Open in New Tab</button>
  728. </div>
  729. </div>
  730. <div class="header-right">
  731. <a href="{pastebin_link}" target="_blank" class="script-link">Open Python Script in New Tab</a>
  732. </div>
  733. </div>
  734. </div>
  735. <div class="container">
  736. <div class="main-content">
  737. <div class="video-player">
  738. <img src="{thumbnail_url}" alt="Video Thumbnail">
  739. </div>
  740. <h1 class="video-title">{title}</h1>
  741. <div class="channel-info">
  742. <img src="{channel_pfp}" class="channel-pfp" alt="N/A">
  743. <div>
  744. <div class="channel-name"><a href="{channel_url}" target="_blank">{channel_name}</a>{' ✔' if data.get('channel_is_verified', False) else ''}</div>
  745. <div class="subscriber-count">{subscriber_count} subscribers</div>
  746. </div>
  747. <div class="like-container">
  748. <!-- Subcontainer for the like box & bar (stacked) -->
  749. <div class="like-subcontainer">
  750. <div class="like-box">
  751. <span>👍 {likes}</span> {dislike_display}
  752. </div>
  753. <div class="like-bar-container">
  754. {like_bar}
  755. </div>
  756. </div>
  757. <div class="copy-info-box">
  758. <div class="dropdown">
  759. <button onclick="incrementLikes()">Copy Info</button>
  760. <div class="dropdown-content">
  761. <button onclick="navigator.clipboard.writeText('{video_url}');">Video URL</button>
  762. <button onclick="navigator.clipboard.writeText('{video_id}');">Video ID</button>
  763. <button onclick="navigator.clipboard.writeText('{channel_url}');">Channel URL</button>
  764. <button onclick="navigator.clipboard.writeText('{channel_id}');">Channel ID</button>
  765. <button onclick="navigator.clipboard.writeText('{uploader_url}');">Channel Handle URL</button>
  766. </div>
  767. </div>
  768. </div>
  769. </div>
  770. </div>
  771. <div class="description collapsed" onclick="expandDescription(this)">
  772. <div style="font-weight: bold; float: left;">{view_count} views • {upload_date}{timestamp_display}</div>
  773. <div style="clear: both; padding-top: 5px;">{description}</div>
  774. <span class="toggle" onclick="event.stopPropagation(); toggleDescription(this.parentElement)">Show more</span>
  775. </div>
  776. {'<p style="font-weight: bold;">Age-restricted video</p>' if age_limit > 0 else ''}
  777. {comment_section}
  778. <div class="additional-info">
  779. <h3>Additional Info</h3>
  780. <p><strong>Full Title:</strong> {fulltitle}</p>
  781. <p><strong>Tags:</strong> {tags}</p>
  782. <p><strong>Categories:</strong> {categories}</p>
  783. <p><strong>Video Length:</strong> {duration_string}</p>
  784. <p><strong>Duration (Seconds):</strong> {duration}</p>
  785. <p><strong>Thumbnail URL:</strong> <a href="{thumbnail_url}" target="_blank">{thumbnail_url}</a></p>
  786. <p><strong>Availability:</strong> {availability}</p>
  787. <p> </p>
  788. <strong>▼ Specific to the Video File ▼</strong>
  789. <p> </p>
  790. <p><strong>Approx. Filesize (Bytes):</strong> {filesize_approx}</p>
  791. <p><strong>Resolution:</strong> {resolution}</p>
  792. <p><strong>FPS:</strong> {fps}</p>
  793. <p><strong>Video Codec:</strong> {vcodec}</p>
  794. <p><strong>Audio Codec:</strong> {acodec}</p>
  795. <p><strong>Format:</strong> {format_}</p>
  796. <p><strong>Format ID:</strong> {format_id}</p>
  797. <p><strong>Format Note:</strong> {format_note}</p>
  798. <p><strong>Video Extension:</strong> {video_ext}</p>
  799. <p><strong>File Extension:</strong> {ext}</p>
  800. <p><strong>Container:</strong> {container}</p>
  801. <p><strong>Dynamic Range:</strong> {dynamic_range}</p>
  802. <p><strong>Total Bitrate:</strong> {tbr}</p>
  803. <p><strong>Average Bitrate:</strong> {abr}</p>
  804. <p><strong>Variable Bitrate:</strong> {vbr}</p>
  805. <p><strong>Audio Sample Rate:</strong> {asr}</p>
  806. </div>
  807. </div>
  808. <div class="sidebar">
  809. <div class="tutorial-links-container">
  810. <div class="tutorial-links">
  811. <p><a target="_blank" href="https://docs.google.com/document/d/1nzr8p1-hBfTq3Tv98d5-yTnGEBxPUDVIcFi90s0HYvs" class="tutorial-placeholder">yt-dlp Tutorial Mega-Document</a></p>
  812. <p><a target="_blank" href="https://docs.google.com/document/d/1adqUaJ9jmQVirhZdLm49p5hyMr-sPDLHvWHLye_Ejog" class="tutorial-placeholder">Rapid Clipping Tutorial Document</a></p>
  813. </div>
  814. <div class="file-modified">
  815. <strong>JSON File Creation Date:</strong> {mod_str}
  816. <span class="tooltip">Based on Date Modified Value</span>
  817. </div>
  818. </div>
  819. </div>
  820. </div>
  821. </div>
  822. <script>
  823. function incrementLikes() {{
  824. let likesSpan = document.querySelector('.like-box span');
  825. let likes = parseInt(likesSpan.textContent.replace('👍 ', '').replace(/,/g, '')) + 1;
  826. likesSpan.textContent = '👍 ' + likes.toLocaleString();
  827. }}
  828.  
  829. function expandDescription(element) {{
  830. // Clicking the big description area only expands if it's collapsed.
  831. if (element.classList.contains('collapsed')) {{
  832. element.classList.remove('collapsed');
  833. element.querySelector('.toggle').textContent = 'Show less';
  834. }}
  835. }}
  836.  
  837. function toggleDescription(element) {{
  838. // If it's collapsed, expand it. Otherwise, collapse it.
  839. if (element.classList.contains('collapsed')) {{
  840. element.classList.remove('collapsed');
  841. element.querySelector('.toggle').textContent = 'Show less';
  842. }} else {{
  843. element.classList.add('collapsed');
  844. element.querySelector('.toggle').textContent = 'Show more';
  845. }}
  846. }}
  847.  
  848. function toggleReplies(element) {{
  849. let replies = element.nextElementSibling;
  850. if (replies.style.display === 'none' || replies.style.display === '') {{
  851. replies.style.display = 'block';
  852. element.textContent = element.textContent.replace('▼', '▲');
  853. }} else {{
  854. replies.style.display = 'none';
  855. element.textContent = element.textContent.replace('▲', '▼');
  856. }}
  857. }}
  858.  
  859. function sortComments(method) {{
  860. let container = document.getElementById('comments-container');
  861. if (!container) return;
  862. let comments = Array.from(container.getElementsByClassName('comment')).filter(c => !c.classList.contains('reply'));
  863.  
  864. comments.sort((a, b) => {{
  865. let textA = a.querySelector('p').textContent.trim().toLowerCase();
  866. let textB = b.querySelector('p').textContent.trim().toLowerCase();
  867. if (method === 'alpha') {{
  868. return textA.localeCompare(textB);
  869. }} else if (method === 'replies') {{
  870. let repliesA = a.querySelector('.reply-toggle') ? parseInt(a.querySelector('.reply-toggle').dataset.replies) : 0;
  871. let repliesB = b.querySelector('.reply-toggle') ? parseInt(b.querySelector('.reply-toggle').dataset.replies) : 0;
  872. return repliesB - repliesA || textA.localeCompare(textB);
  873. }} else if (method === 'length') {{
  874. let lengthA = a.querySelector('p').textContent.length;
  875. let lengthB = b.querySelector('p').textContent.length;
  876. return lengthB - lengthA || textA.localeCompare(textB);
  877. }} else {{ // Most Likes
  878. let likesA = parseInt(a.querySelector('.comment-likes').textContent.replace('👍 ', '').replace(/,/g, '')) || 0;
  879. let likesB = parseInt(b.querySelector('.comment-likes').textContent.replace('👍 ', '').replace(/,/g, '')) || 0;
  880. return likesB - likesA || textA.localeCompare(textB);
  881. }}
  882. }});
  883.  
  884. container.innerHTML = '';
  885. comments.forEach(comment => container.appendChild(comment));
  886. }}
  887.  
  888. function setupDropdownReset() {{
  889. let dropdownButtons = document.querySelectorAll('.dropdown-content button');
  890. dropdownButtons.forEach(button => {{
  891. button.addEventListener('click', function() {{
  892. // Hide the dropdown temporarily
  893. let dropdownContent = this.parentElement;
  894. dropdownContent.style.display = 'none';
  895. // After a short delay, remove the inline style so .dropdown:hover can work again
  896. setTimeout(() => {{
  897. dropdownContent.removeAttribute('style');
  898. }}, 150);
  899. }});
  900. }});
  901. }}
  902. window.onload = setupDropdownReset;
  903. </script>
  904. </body>
  905. </html>
  906. """
  907.  
  908. # Write HTML file
  909. output_filename = os.path.splitext(filename)[0] + ".html"
  910. with open(os.path.join(directory, output_filename), 'w', encoding='utf-8') as f:
  911. f.write(html_content)
  912.  
  913. print("HTML files generated successfully.")
  914.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement