manish

discord chat view

Oct 11th, 2026
102
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 9.23 KB | None | 0 0
  1. import os
  2. import glob
  3. import re
  4. import sys
  5. import urllib.request
  6. import urllib.parse
  7. import html  # Add this new import!
  8.  
  9. def get_channel_id(filename):
  10.     match = re.search(r'\[(\d+)\]\.html$', filename)
  11.     if match:
  12.         return int(match.group(1))
  13.     return float('inf')
  14.  
  15.  
  16. def download_and_localize_images(target_dir, log_files):
  17.     img_dir_name = "images"
  18.     img_dir_path = os.path.join(target_dir, img_dir_name)
  19.     os.makedirs(img_dir_path, exist_ok=True)
  20.  
  21.     downloaded_urls = {}
  22.     img_counter = 0
  23.  
  24.     print("Scanning log files for external images to download locally...")
  25.     src_pattern = re.compile(r'src=["\'](https?://[^"\']+)["\']', re.IGNORECASE)
  26.  
  27.     for file_path in log_files:
  28.         filename = os.path.basename(file_path)
  29.         print(f"  Processing image links in: {filename}")
  30.  
  31.         with open(file_path, "r", encoding="utf-8") as f:
  32.             html_content = f.read()
  33.  
  34.         urls = src_pattern.findall(html_content)
  35.         if not urls:
  36.             continue
  37.  
  38.         modified = False
  39.         for url in urls:
  40.             if url.startswith(('data:', 'file:', './')) or url in downloaded_urls:
  41.                 if url in downloaded_urls:
  42.                     html_content = html_content.replace(url, downloaded_urls[url])
  43.                     modified = True
  44.                 continue
  45.  
  46.             try:
  47.                 parsed_url = urllib.parse.urlparse(url)
  48.                 clean_path = parsed_url.path
  49.                 ext = os.path.splitext(clean_path)[1]
  50.  
  51.                 if '?' in ext:
  52.                     ext = ext.split('?')[0]
  53.                 if not ext or len(ext) > 5:
  54.                     ext = ".png"
  55.  
  56.                 img_counter += 1
  57.                 local_img_name = f"img_{img_counter:04d}{ext}"
  58.                 local_img_path = os.path.join(img_dir_path, local_img_name)
  59.  
  60.                 req = urllib.request.Request(
  61.                     html.unescape(url),
  62.                     headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
  63.                 )
  64.  
  65.                 with urllib.request.urlopen(req, timeout=10) as response, open(local_img_path, "wb") as out_file:
  66.                     out_file.write(response.read())
  67.  
  68.                 relative_local_path = f"{img_dir_name}/{local_img_name}"
  69.                 downloaded_urls[url] = relative_local_path
  70.                 html_content = html_content.replace(url, relative_local_path)
  71.                 modified = True
  72.  
  73.             except Exception as e:
  74.                 print(f"    Warning: Could not download asset {url}... Error: {e}")
  75.  
  76.         if modified:
  77.             with open(file_path, "w", encoding="utf-8") as f:
  78.                 f.write(html_content)
  79.  
  80.     print(f"Successfully cached {img_counter} image assets locally.")
  81.  
  82.  
  83. def build_discord_archive(target_dir, game_name):
  84.     target_dir = os.path.abspath(target_dir)
  85.  
  86.     if not os.path.isdir(target_dir):
  87.         print(f"Error: The directory '{target_dir}' does not exist.")
  88.         return
  89.  
  90.     all_html_files = glob.glob(os.path.join(target_dir, "*.html"))
  91.     exclude_files = {"index.html", "sidebar.html", "welcome.html", "public_index.html"}
  92.     log_files = [f for f in all_html_files if os.path.basename(f) not in exclude_files]
  93.  
  94.     if not log_files:
  95.         print(f"No Discord chat log HTML files found in '{target_dir}'.")
  96.         return
  97.  
  98.     # Download image resources locally
  99.     download_and_localize_images(target_dir, log_files)
  100.  
  101.     # Sort files chronologically by channel ID
  102.     log_files.sort(key=lambda x: get_channel_id(os.path.basename(x)))
  103.  
  104.     print(f"Compiling Single Page Application layout for '{game_name}'...")
  105.  
  106.     sidebar_buttons = []
  107.     chat_panels = []
  108.  
  109.     for idx, file_path in enumerate(log_files):
  110.         filename = os.path.basename(file_path)
  111.  
  112.         clean_name = filename.split(" - ")[-1].split(" [")[0]
  113.         if not clean_name or clean_name.endswith(".html"):
  114.             clean_name = os.path.splitext(filename)[0]
  115.         clean_name = clean_name.replace('"', '"')
  116.  
  117.         with open(file_path, "r", encoding="utf-8") as f:
  118.             raw_html = f.read()
  119.  
  120.         body_match = re.search(r"<body[^>]*>(.*)</body>", raw_html, re.DOTALL | re.IGNORECASE)
  121.         body_content = body_match.group(1) if body_match else raw_html
  122.  
  123.         panel_id = f"chan_panel_{idx}"
  124.  
  125.         btn_html = f'        <button class="channel-link" onclick="switchChannel(\'{panel_id}\', this)">{clean_name}</button>'
  126.         sidebar_buttons.append(btn_html)
  127.  
  128.         panel_html = f'        <div id="{panel_id}" class="chat-container-view" style="display: none;">\n{body_content}\n        </div>'
  129.         chat_panels.append(panel_html)
  130.  
  131.     sidebar_content_block = "\n".join(sidebar_buttons)
  132.     chat_panels_block = "\n".join(chat_panels)
  133.  
  134.     master_spa_tmpl = f"""<!DOCTYPE html>
  135. <html lang="en">
  136. <head>
  137.    <meta charset="UTF-8">
  138.    <title>{game_name} - Chat Logs</title>
  139.    <style>
  140.        * {{ box-sizing: border-box; margin: 0; padding: 0; }}
  141.        body, html {{ height: 100%; background-color: #202225; overflow: hidden; display: flex; }}
  142.  
  143.        /* Sidebar layout styles */
  144.        .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; }}
  145.        .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; }}
  146.        .channel-list {{ padding: 0 8px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 2px; }}
  147.  
  148.        .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; }}
  149.        .channel-link::before {{ content: "# "; font-size: 1.1rem; color: #4f545c; margin-right: 4px; }}
  150.        .channel-link:hover {{ background-color: #34363c; color: #dcddde; }}
  151.        .channel-link.active {{ background-color: #393c43; color: #ffffff; }}
  152.        .channel-link.active::before {{ color: #b9bbbe; }}
  153.  
  154.        /* Expansive content layout layers */
  155.        .main-viewer {{ flex: 1; height: 100%; background-color: #36393f; position: relative; }}
  156.        .chat-container-view {{ width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden; }}
  157.  
  158.        /* Retain standard Discord layout width rules cleanly across screen space */
  159.        .preamble, .chatlog, .postamble {{ max-width: 100% !important; width: 100% !important; }}
  160.  
  161.        /* Fixed Welcome Screen Layout centering elements safely */
  162.        .welcome-screen {{ display: flex; align-items: center; justify-content: center; height: 100%; width: 100%; color: #72767d; font-family: sans-serif; padding: 20px; }}
  163.        .welcome-container {{ max-width: 500px; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; }}
  164.        .welcome-container h2 {{ color: #b9bbbe; margin-bottom: 12px; font-size: 1.6rem; text-align: center !important; width: 100%; }}
  165.        .welcome-container p {{ font-size: 0.95rem; line-height: 1.5; text-align: center !important; width: 100%; }}
  166.    </style>
  167. </head>
  168. <body>
  169.  
  170.    <div class="sidebar">
  171.        <div class="sidebar-header">Text Channels</div>
  172.        <div class="channel-list">
  173. {sidebar_content_block}
  174.        </div>
  175.    </div>
  176.  
  177.    <div class="main-viewer">
  178.        <div id="welcome-layer" class="welcome-screen">
  179.            <div class="welcome-container">
  180.                <h2>Welcome to the {game_name} Archive</h2>
  181.                <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>
  182.            </div>
  183.        </div>
  184.  
  185.        {chat_panels_block}
  186.    </div>
  187.  
  188.    <script>
  189.        function switchChannel(targetPanelId, activeBtn) {{
  190.            // Hide the welcome block layer
  191.            document.getElementById('welcome-layer').style.display = 'none';
  192.  
  193.            // Hide all other viewport logs
  194.            document.querySelectorAll('.chat-container-view').forEach(panel => {{
  195.                panel.style.display = 'none';
  196.            }});
  197.  
  198.            // Clear existing navbar highlight tracks
  199.            document.querySelectorAll('.channel-link').forEach(btn => {{
  200.                btn.classList.remove('active');
  201.            }});
  202.  
  203.            // Activate target visibility layers
  204.            document.getElementById(targetPanelId).style.display = 'block';
  205.            activeBtn.classList.add('active');
  206.        }}
  207.    </script>
  208. </body>
  209. </html>"""
  210.  
  211.     output_path = os.path.join(target_dir, "index.html")
  212.     with open(output_path, "w", encoding="utf-8") as f:
  213.         f.write(master_spa_tmpl)
  214.  
  215.     print(f"\nSuccess! Built your instant-switch, scroll-preserving archive sheet at:\n{output_path}")
  216.  
  217.  
  218. if __name__ == "__main__":
  219.     if len(sys.argv) > 2:
  220.         path_input = sys.argv[1]
  221.         game_input = sys.argv[2]
  222.     else:
  223.         path_input = input("Enter the path to your Discord logs directory: ").strip()
  224.         game_input = input("Enter the name of the game: ").strip()
  225.  
  226.     build_discord_archive(path_input, game_input)
Advertisement