Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import os
- import glob
- import re
- import sys
- import urllib.request
- import urllib.parse
- import html # Add this new import!
- def get_channel_id(filename):
- match = re.search(r'\[(\d+)\]\.html$', filename)
- if match:
- return int(match.group(1))
- return float('inf')
- def download_and_localize_images(target_dir, log_files):
- img_dir_name = "images"
- img_dir_path = os.path.join(target_dir, img_dir_name)
- os.makedirs(img_dir_path, exist_ok=True)
- downloaded_urls = {}
- img_counter = 0
- print("Scanning log files for external images to download locally...")
- src_pattern = re.compile(r'src=["\'](https?://[^"\']+)["\']', re.IGNORECASE)
- for file_path in log_files:
- filename = os.path.basename(file_path)
- print(f" Processing image links in: {filename}")
- with open(file_path, "r", encoding="utf-8") as f:
- html_content = f.read()
- urls = src_pattern.findall(html_content)
- if not urls:
- continue
- modified = False
- for url in urls:
- if url.startswith(('data:', 'file:', './')) or url in downloaded_urls:
- if url in downloaded_urls:
- html_content = html_content.replace(url, downloaded_urls[url])
- modified = True
- continue
- try:
- parsed_url = urllib.parse.urlparse(url)
- clean_path = parsed_url.path
- ext = os.path.splitext(clean_path)[1]
- if '?' in ext:
- ext = ext.split('?')[0]
- if not ext or len(ext) > 5:
- ext = ".png"
- img_counter += 1
- local_img_name = f"img_{img_counter:04d}{ext}"
- local_img_path = os.path.join(img_dir_path, local_img_name)
- req = urllib.request.Request(
- html.unescape(url),
- headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
- )
- with urllib.request.urlopen(req, timeout=10) as response, open(local_img_path, "wb") as out_file:
- out_file.write(response.read())
- relative_local_path = f"{img_dir_name}/{local_img_name}"
- downloaded_urls[url] = relative_local_path
- html_content = html_content.replace(url, relative_local_path)
- modified = True
- except Exception as e:
- print(f" Warning: Could not download asset {url}... Error: {e}")
- if modified:
- with open(file_path, "w", encoding="utf-8") as f:
- f.write(html_content)
- print(f"Successfully cached {img_counter} image assets locally.")
- def build_discord_archive(target_dir, game_name):
- target_dir = os.path.abspath(target_dir)
- if not os.path.isdir(target_dir):
- print(f"Error: The directory '{target_dir}' does not exist.")
- return
- all_html_files = glob.glob(os.path.join(target_dir, "*.html"))
- exclude_files = {"index.html", "sidebar.html", "welcome.html", "public_index.html"}
- log_files = [f for f in all_html_files if os.path.basename(f) not in exclude_files]
- if not log_files:
- print(f"No Discord chat log HTML files found in '{target_dir}'.")
- return
- # Download image resources locally
- download_and_localize_images(target_dir, log_files)
- # Sort files chronologically by channel ID
- log_files.sort(key=lambda x: get_channel_id(os.path.basename(x)))
- print(f"Compiling Single Page Application layout for '{game_name}'...")
- sidebar_buttons = []
- chat_panels = []
- for idx, file_path in enumerate(log_files):
- filename = os.path.basename(file_path)
- clean_name = filename.split(" - ")[-1].split(" [")[0]
- if not clean_name or clean_name.endswith(".html"):
- clean_name = os.path.splitext(filename)[0]
- clean_name = clean_name.replace('"', '"')
- with open(file_path, "r", encoding="utf-8") as f:
- raw_html = f.read()
- body_match = re.search(r"<body[^>]*>(.*)</body>", raw_html, re.DOTALL | re.IGNORECASE)
- body_content = body_match.group(1) if body_match else raw_html
- panel_id = f"chan_panel_{idx}"
- btn_html = f' <button class="channel-link" onclick="switchChannel(\'{panel_id}\', this)">{clean_name}</button>'
- sidebar_buttons.append(btn_html)
- panel_html = f' <div id="{panel_id}" class="chat-container-view" style="display: none;">\n{body_content}\n </div>'
- chat_panels.append(panel_html)
- sidebar_content_block = "\n".join(sidebar_buttons)
- chat_panels_block = "\n".join(chat_panels)
- master_spa_tmpl = f"""<!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>{game_name} - Chat Logs</title>
- <style>
- * {{ box-sizing: border-box; margin: 0; padding: 0; }}
- body, html {{ height: 100%; background-color: #202225; overflow: hidden; display: flex; }}
- /* Sidebar layout styles */
- .sidebar {{ width: 260px; background-color: #2f3136; height: 100%; display: flex; flex-direction: column; border-right: 1px solid rgba(255, 255, 255, 0.05); flex-shrink: 0; }}
- .sidebar-header {{ padding: 1rem; color: #ffffff; font-weight: 600; font-size: 1.1rem; border-bottom: 1px solid rgba(0, 0, 0, 0.2); margin-bottom: 10px; }}
- .channel-list {{ padding: 0 8px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 2px; }}
- .channel-link {{ display: flex; align-items: center; padding: 8px 12px; color: #8e9297; background: none; border: none; border-radius: 4px; font-size: 0.95rem; font-weight: 500; text-align: left; width: 100%; cursor: pointer; transition: background-color 0.1s, color 0.1s; }}
- .channel-link::before {{ content: "# "; font-size: 1.1rem; color: #4f545c; margin-right: 4px; }}
- .channel-link:hover {{ background-color: #34363c; color: #dcddde; }}
- .channel-link.active {{ background-color: #393c43; color: #ffffff; }}
- .channel-link.active::before {{ color: #b9bbbe; }}
- /* Expansive content layout layers */
- .main-viewer {{ flex: 1; height: 100%; background-color: #36393f; position: relative; }}
- .chat-container-view {{ width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden; }}
- /* Retain standard Discord layout width rules cleanly across screen space */
- .preamble, .chatlog, .postamble {{ max-width: 100% !important; width: 100% !important; }}
- /* Fixed Welcome Screen Layout centering elements safely */
- .welcome-screen {{ display: flex; align-items: center; justify-content: center; height: 100%; width: 100%; color: #72767d; font-family: sans-serif; padding: 20px; }}
- .welcome-container {{ max-width: 500px; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; }}
- .welcome-container h2 {{ color: #b9bbbe; margin-bottom: 12px; font-size: 1.6rem; text-align: center !important; width: 100%; }}
- .welcome-container p {{ font-size: 0.95rem; line-height: 1.5; text-align: center !important; width: 100%; }}
- </style>
- </head>
- <body>
- <div class="sidebar">
- <div class="sidebar-header">Text Channels</div>
- <div class="channel-list">
- {sidebar_content_block}
- </div>
- </div>
- <div class="main-viewer">
- <div id="welcome-layer" class="welcome-screen">
- <div class="welcome-container">
- <h2>Welcome to the {game_name} Archive</h2>
- <p>Select a channel from the sidebar list on the left to view specific round logs. Your scroll positions will remain locked automatically as you switch.</p>
- </div>
- </div>
- {chat_panels_block}
- </div>
- <script>
- function switchChannel(targetPanelId, activeBtn) {{
- // Hide the welcome block layer
- document.getElementById('welcome-layer').style.display = 'none';
- // Hide all other viewport logs
- document.querySelectorAll('.chat-container-view').forEach(panel => {{
- panel.style.display = 'none';
- }});
- // Clear existing navbar highlight tracks
- document.querySelectorAll('.channel-link').forEach(btn => {{
- btn.classList.remove('active');
- }});
- // Activate target visibility layers
- document.getElementById(targetPanelId).style.display = 'block';
- activeBtn.classList.add('active');
- }}
- </script>
- </body>
- </html>"""
- output_path = os.path.join(target_dir, "index.html")
- with open(output_path, "w", encoding="utf-8") as f:
- f.write(master_spa_tmpl)
- print(f"\nSuccess! Built your instant-switch, scroll-preserving archive sheet at:\n{output_path}")
- if __name__ == "__main__":
- if len(sys.argv) > 2:
- path_input = sys.argv[1]
- game_input = sys.argv[2]
- else:
- path_input = input("Enter the path to your Discord logs directory: ").strip()
- game_input = input("Enter the name of the game: ").strip()
- build_discord_archive(path_input, game_input)
Advertisement