| @@ -22,6 +22,660 @@ def convert_whatsapp_timestamp(ts): | |||||
| except (ValueError, TypeError): | except (ValueError, TypeError): | ||||
| return "Invalid date" | return "Invalid date" | ||||
| def get_media_tag_for_gallery(media_path, output_dir, base_path=""): | |||||
| """Generates media HTML for gallery pages with proper relative paths.""" | |||||
| if not media_path: | |||||
| return "" | |||||
| test_path = os.path.join(output_dir, 'Message', media_path) | |||||
| if not os.path.exists(test_path): | |||||
| return "" | |||||
| full_media_path = os.path.join(base_path, 'Message', media_path) | |||||
| full_media_path = full_media_path.lstrip('./') | |||||
| full_media_path = f'../{full_media_path}' | |||||
| ext = os.path.splitext(media_path)[1].lower() | |||||
| if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: | |||||
| return f'<img src="{full_media_path}" loading="lazy" alt="Image" class="gallery-media">' | |||||
| elif ext in ['.mp4', '.mov', '.webm']: | |||||
| return f'<video controls src="{full_media_path}" class="gallery-media" loading="lazy"></video>' | |||||
| elif ext in ['.mp3', '.ogg', '.opus', '.m4a']: | |||||
| return f'<audio controls src="{full_media_path}" loading="lazy"></audio>' | |||||
| else: | |||||
| return f'<div class="file-media"><a href="{full_media_path}" target="_blank">📎 {os.path.basename(media_path)}</a></div>' | |||||
| def generate_all_media_gallery(db_path, output_dir): | |||||
| """Generates HTML pages showing all media files sorted by time with pagination.""" | |||||
| conn = sqlite3.connect(db_path) | |||||
| cursor = conn.cursor() | |||||
| # Get all media messages with chat and sender information | |||||
| query = """ | |||||
| SELECT | |||||
| m.ZMESSAGEDATE, | |||||
| mi.ZMEDIALOCALPATH, | |||||
| m.ZISFROMME, | |||||
| m.ZFROMJID, | |||||
| cs.ZPARTNERNAME AS ChatName, | |||||
| cs.ZCONTACTJID, | |||||
| gm_p.ZPUSHNAME AS GroupMemberName, | |||||
| p.ZPUSHNAME AS PushName, | |||||
| cs.Z_PK as ChatID, | |||||
| gm.ZMEMBERJID AS GroupMemberJID, | |||||
| sender_cs.ZPARTNERNAME AS SenderPartnerName, | |||||
| gm_fallback_p.ZPUSHNAME AS GroupMemberNameFallback | |||||
| FROM | |||||
| ZWAMESSAGE m | |||||
| LEFT JOIN | |||||
| ZWAMEDIAITEM mi ON m.ZMEDIAITEM = mi.Z_PK | |||||
| LEFT JOIN | |||||
| ZWACHATSESSION cs ON m.ZCHATSESSION = cs.Z_PK | |||||
| LEFT JOIN | |||||
| ZWAGROUPMEMBER gm ON gm.Z_PK = m.ZGROUPMEMBER | |||||
| LEFT JOIN | |||||
| ZWAPROFILEPUSHNAME gm_p ON gm.ZMEMBERJID = gm_p.ZJID | |||||
| LEFT JOIN | |||||
| ZWAPROFILEPUSHNAME p ON m.ZFROMJID = p.ZJID | |||||
| LEFT JOIN | |||||
| ZWACHATSESSION sender_cs ON sender_cs.ZCONTACTJID = m.ZFROMJID | |||||
| LEFT JOIN | |||||
| ZWAGROUPMEMBER gm_fallback ON gm_fallback.ZCHATSESSION = m.ZCHATSESSION AND gm_fallback.ZMEMBERJID = m.ZFROMJID | |||||
| LEFT JOIN | |||||
| ZWAPROFILEPUSHNAME gm_fallback_p ON gm_fallback.ZMEMBERJID = gm_fallback_p.ZJID | |||||
| WHERE | |||||
| mi.ZMEDIALOCALPATH IS NOT NULL | |||||
| AND cs.ZCONTACTJID NOT LIKE '%@status' | |||||
| ORDER BY | |||||
| m.ZMESSAGEDATE DESC; | |||||
| """ | |||||
| cursor.execute(query) | |||||
| all_media_messages = cursor.fetchall() | |||||
| conn.close() | |||||
| # Filter out messages without valid media paths | |||||
| valid_media_messages = [] | |||||
| for msg in all_media_messages: | |||||
| if msg[1] and os.path.exists(os.path.join(output_dir, 'Message', msg[1])): | |||||
| valid_media_messages.append(msg) | |||||
| total_media = len(valid_media_messages) | |||||
| items_per_page = 120 # Show 120 media items per page | |||||
| total_pages = (total_media + items_per_page - 1) // items_per_page | |||||
| if total_pages == 0: | |||||
| total_pages = 1 | |||||
| # Generate each page | |||||
| for page_num in range(1, total_pages + 1): | |||||
| start_idx = (page_num - 1) * items_per_page | |||||
| end_idx = min(start_idx + items_per_page, total_media) | |||||
| page_media_messages = valid_media_messages[start_idx:end_idx] | |||||
| # Determine filename | |||||
| if page_num == 1: | |||||
| filename = "media-gallery.html" | |||||
| else: | |||||
| filename = f"media-gallery-page-{page_num}.html" | |||||
| media_gallery_path = os.path.join(output_dir, filename) | |||||
| with open(media_gallery_path, 'w', encoding='utf-8') as f: | |||||
| f.write(f""" | |||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||||
| <title>WhatsApp Media Gallery - Page {page_num}</title> | |||||
| <style> | |||||
| body {{ | |||||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |||||
| background-color: #f4f4f9; | |||||
| margin: 0; | |||||
| padding: 20px; | |||||
| min-height: 100vh; | |||||
| }} | |||||
| .header {{ | |||||
| background-color: #128C7E; | |||||
| color: white; | |||||
| padding: 20px; | |||||
| margin: -20px -20px 20px -20px; | |||||
| text-align: center; | |||||
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |||||
| }} | |||||
| .header h1 {{ | |||||
| margin: 0; | |||||
| font-size: 1.8em; | |||||
| }} | |||||
| .nav-links {{ | |||||
| margin-top: 10px; | |||||
| }} | |||||
| .nav-links a {{ | |||||
| color: rgba(255,255,255,0.9); | |||||
| text-decoration: none; | |||||
| margin: 0 10px; | |||||
| padding: 5px 10px; | |||||
| border-radius: 5px; | |||||
| transition: background-color 0.2s; | |||||
| }} | |||||
| .nav-links a:hover {{ | |||||
| background-color: rgba(255,255,255,0.1); | |||||
| }} | |||||
| .container {{ | |||||
| max-width: 1200px; | |||||
| margin: auto; | |||||
| background: white; | |||||
| padding: 20px; | |||||
| border-radius: 12px; | |||||
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |||||
| }} | |||||
| .page-info {{ | |||||
| text-align: center; | |||||
| margin-bottom: 20px; | |||||
| color: #666; | |||||
| font-size: 0.9em; | |||||
| }} | |||||
| .pagination {{ | |||||
| display: flex; | |||||
| justify-content: center; | |||||
| align-items: center; | |||||
| gap: 10px; | |||||
| margin: 20px 0; | |||||
| flex-wrap: wrap; | |||||
| }} | |||||
| .pagination a, .pagination span {{ | |||||
| padding: 8px 12px; | |||||
| border-radius: 6px; | |||||
| text-decoration: none; | |||||
| font-size: 0.9em; | |||||
| min-width: 35px; | |||||
| text-align: center; | |||||
| transition: all 0.2s; | |||||
| }} | |||||
| .pagination a {{ | |||||
| background-color: #f8f9fa; | |||||
| color: #128C7E; | |||||
| border: 1px solid #dee2e6; | |||||
| }} | |||||
| .pagination a:hover {{ | |||||
| background-color: #128C7E; | |||||
| color: white; | |||||
| }} | |||||
| .pagination .current {{ | |||||
| background-color: #128C7E; | |||||
| color: white; | |||||
| border: 1px solid #128C7E; | |||||
| }} | |||||
| .pagination .disabled {{ | |||||
| background-color: #e9ecef; | |||||
| color: #6c757d; | |||||
| border: 1px solid #dee2e6; | |||||
| cursor: not-allowed; | |||||
| }} | |||||
| .media-grid {{ | |||||
| display: grid; | |||||
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |||||
| gap: 20px; | |||||
| margin-top: 20px; | |||||
| }} | |||||
| .media-item {{ | |||||
| background: #f8f9fa; | |||||
| border-radius: 12px; | |||||
| padding: 15px; | |||||
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |||||
| }} | |||||
| .media-header {{ | |||||
| display: flex; | |||||
| align-items: center; | |||||
| margin-bottom: 10px; | |||||
| font-size: 0.9em; | |||||
| color: #666; | |||||
| }} | |||||
| .chat-name {{ | |||||
| font-weight: 500; | |||||
| color: #128C7E; | |||||
| }} | |||||
| .sender-name {{ | |||||
| color: #005c4b; | |||||
| margin-left: 5px; | |||||
| }} | |||||
| .timestamp {{ | |||||
| margin-left: auto; | |||||
| font-size: 0.8em; | |||||
| color: #999; | |||||
| }} | |||||
| .gallery-media {{ | |||||
| width: 100%; | |||||
| max-height: 300px; | |||||
| object-fit: cover; | |||||
| border-radius: 8px; | |||||
| }} | |||||
| .raw-file-link {{ | |||||
| display: inline-block; | |||||
| margin-top: 8px; | |||||
| padding: 4px 8px; | |||||
| background: #128C7E; | |||||
| color: white; | |||||
| text-decoration: none; | |||||
| border-radius: 4px; | |||||
| font-size: 0.75em; | |||||
| transition: background-color 0.2s; | |||||
| }} | |||||
| .raw-file-link:hover {{ | |||||
| background: #0d6b5e; | |||||
| color: white; | |||||
| text-decoration: none; | |||||
| }} | |||||
| .file-media {{ | |||||
| padding: 20px; | |||||
| text-align: center; | |||||
| background: #e9ecef; | |||||
| border-radius: 8px; | |||||
| border: 2px dashed #adb5bd; | |||||
| }} | |||||
| .file-media a {{ | |||||
| text-decoration: none; | |||||
| color: #495057; | |||||
| font-weight: 500; | |||||
| }} | |||||
| </style> | |||||
| </head> | |||||
| <body> | |||||
| <div class="header"> | |||||
| <h1>📷 Media Gallery</h1> | |||||
| <div class="nav-links"> | |||||
| <a href="whatsapp-chats.html">← Back to Chats</a> | |||||
| <a href="index.html">🏠 Home</a> | |||||
| </div> | |||||
| </div> | |||||
| <div class="container"> | |||||
| <div class="page-info"> | |||||
| Showing {start_idx + 1}-{end_idx} of {total_media} media files (Page {page_num} of {total_pages}) | |||||
| </div> | |||||
| """) | |||||
| # Add pagination controls | |||||
| f.write('<div class="pagination">') | |||||
| # Previous page link | |||||
| if page_num > 1: | |||||
| prev_filename = "media-gallery.html" if page_num == 2 else f"media-gallery-page-{page_num - 1}.html" | |||||
| f.write(f'<a href="{prev_filename}">‹ Previous</a>') | |||||
| else: | |||||
| f.write('<span class="disabled">‹ Previous</span>') | |||||
| # Page numbers | |||||
| start_page = max(1, page_num - 2) | |||||
| end_page = min(total_pages, page_num + 2) | |||||
| if start_page > 1: | |||||
| f.write('<a href="media-gallery.html">1</a>') | |||||
| if start_page > 2: | |||||
| f.write('<span>...</span>') | |||||
| for p in range(start_page, end_page + 1): | |||||
| if p == page_num: | |||||
| f.write(f'<span class="current">{p}</span>') | |||||
| else: | |||||
| p_filename = "media-gallery.html" if p == 1 else f"media-gallery-page-{p}.html" | |||||
| f.write(f'<a href="{p_filename}">{p}</a>') | |||||
| if end_page < total_pages: | |||||
| if end_page < total_pages - 1: | |||||
| f.write('<span>...</span>') | |||||
| f.write(f'<a href="media-gallery-page-{total_pages}.html">{total_pages}</a>') | |||||
| # Next page link | |||||
| if page_num < total_pages: | |||||
| next_filename = f"media-gallery-page-{page_num + 1}.html" | |||||
| f.write(f'<a href="{next_filename}">Next ›</a>') | |||||
| else: | |||||
| f.write('<span class="disabled">Next ›</span>') | |||||
| f.write('</div>') | |||||
| # Media grid | |||||
| f.write('<div class="media-grid">') | |||||
| for message_date, media_path, is_from_me, from_jid, chat_name, contact_jid, group_member_name, push_name, chat_id, group_member_jid, sender_partner_name, group_member_name_fallback in page_media_messages: | |||||
| if not media_path: | |||||
| continue | |||||
| # Determine sender name | |||||
| if is_from_me: | |||||
| sender_name = "You" | |||||
| else: | |||||
| # For group messages, prioritize ZCONTACTNAME from ZWAGROUPMEMBER linked via ZGROUPMEMBER | |||||
| sender_name = group_member_name or group_member_name_fallback # Try direct link first, then fallback via ZFROMJID | |||||
| if not sender_name: | |||||
| # Try sender's partner name from their individual chat session | |||||
| sender_name = sender_partner_name or push_name | |||||
| if not sender_name: | |||||
| # Check if this is a group chat and ZFROMJID is the group JID (can't determine individual sender) | |||||
| if '@g.us' in str(contact_jid or '') and from_jid and '@g.us' in from_jid: | |||||
| sender_name = "Group Member" # Generic fallback for unidentifiable group messages | |||||
| elif group_member_jid and '@' in group_member_jid: | |||||
| phone_number = group_member_jid.split('@')[0] | |||||
| sender_name = f"+{phone_number}" if phone_number.isdigit() else group_member_jid | |||||
| elif from_jid and '@' in from_jid: | |||||
| phone_number = from_jid.split('@')[0] | |||||
| sender_name = f"+{phone_number}" if phone_number.isdigit() else from_jid | |||||
| else: | |||||
| sender_name = "Unknown" | |||||
| # Generate media HTML | |||||
| media_html = get_media_tag_for_gallery(media_path, output_dir) | |||||
| if not media_html: | |||||
| continue | |||||
| # Sanitize contact_jid for filename | |||||
| if contact_jid: | |||||
| safe_filename = "".join(c if c.isalnum() else "_" for c in contact_jid) | |||||
| else: | |||||
| safe_filename = str(chat_id) | |||||
| f.write(f""" | |||||
| <div class="media-item"> | |||||
| <div class="media-header"> | |||||
| <span class="chat-name">{html.escape(str(chat_name or "Unknown Chat"))}</span> | |||||
| <span class="sender-name">• {html.escape(str(sender_name))}</span> | |||||
| <span class="timestamp">{convert_whatsapp_timestamp(message_date)}</span> | |||||
| </div> | |||||
| <a href="chats/{safe_filename}.html" style="text-decoration: none; color: inherit;"> | |||||
| {media_html} | |||||
| </a> | |||||
| <a href="../Message/{media_path}" target="_blank" class="raw-file-link">📁 Open File</a> | |||||
| </div> | |||||
| """) | |||||
| f.write('</div>') | |||||
| # Add pagination controls at bottom | |||||
| f.write('<div class="pagination">') | |||||
| # Previous page link | |||||
| if page_num > 1: | |||||
| prev_filename = "media-gallery.html" if page_num == 2 else f"media-gallery-page-{page_num - 1}.html" | |||||
| f.write(f'<a href="{prev_filename}">‹ Previous</a>') | |||||
| else: | |||||
| f.write('<span class="disabled">‹ Previous</span>') | |||||
| # Page numbers (simplified for bottom) | |||||
| for p in range(start_page, end_page + 1): | |||||
| if p == page_num: | |||||
| f.write(f'<span class="current">{p}</span>') | |||||
| else: | |||||
| p_filename = "media-gallery.html" if p == 1 else f"media-gallery-page-{p}.html" | |||||
| f.write(f'<a href="{p_filename}">{p}</a>') | |||||
| # Next page link | |||||
| if page_num < total_pages: | |||||
| next_filename = f"media-gallery-page-{page_num + 1}.html" | |||||
| f.write(f'<a href="{next_filename}">Next ›</a>') | |||||
| else: | |||||
| f.write('<span class="disabled">Next ›</span>') | |||||
| f.write('</div>') | |||||
| f.write(""" | |||||
| </div> | |||||
| </body> | |||||
| </html> | |||||
| """) | |||||
| print(f"Generated {total_pages} media gallery pages with {total_media} total media files") | |||||
| def generate_chat_media_gallery(db_path, output_dir, chat_id, chat_name, contact_jid): | |||||
| """Generates an HTML page showing all media files for a specific chat.""" | |||||
| conn = sqlite3.connect(db_path) | |||||
| cursor = conn.cursor() | |||||
| # Get media messages for this specific chat | |||||
| query = """ | |||||
| SELECT | |||||
| m.ZMESSAGEDATE, | |||||
| mi.ZMEDIALOCALPATH, | |||||
| m.ZISFROMME, | |||||
| m.ZFROMJID, | |||||
| gm_p.ZPUSHNAME AS GroupMemberName, | |||||
| p.ZPUSHNAME AS PushName, | |||||
| gm.ZMEMBERJID AS GroupMemberJID, | |||||
| sender_cs.ZPARTNERNAME AS SenderPartnerName, | |||||
| gm_fallback_p.ZPUSHNAME AS GroupMemberNameFallback | |||||
| FROM | |||||
| ZWAMESSAGE m | |||||
| LEFT JOIN | |||||
| ZWAMEDIAITEM mi ON m.ZMEDIAITEM = mi.Z_PK | |||||
| LEFT JOIN | |||||
| ZWAGROUPMEMBER gm ON gm.Z_PK = m.ZGROUPMEMBER | |||||
| LEFT JOIN | |||||
| ZWAPROFILEPUSHNAME gm_p ON gm.ZMEMBERJID = gm_p.ZJID | |||||
| LEFT JOIN | |||||
| ZWAPROFILEPUSHNAME p ON m.ZFROMJID = p.ZJID | |||||
| LEFT JOIN | |||||
| ZWACHATSESSION sender_cs ON sender_cs.ZCONTACTJID = m.ZFROMJID | |||||
| LEFT JOIN | |||||
| ZWAGROUPMEMBER gm_fallback ON gm_fallback.ZCHATSESSION = m.ZCHATSESSION AND gm_fallback.ZMEMBERJID = m.ZFROMJID | |||||
| LEFT JOIN | |||||
| ZWAPROFILEPUSHNAME gm_fallback_p ON gm_fallback.ZMEMBERJID = gm_fallback_p.ZJID | |||||
| WHERE | |||||
| m.ZCHATSESSION = ? | |||||
| AND mi.ZMEDIALOCALPATH IS NOT NULL | |||||
| ORDER BY | |||||
| m.ZMESSAGEDATE DESC; | |||||
| """ | |||||
| cursor.execute(query, (chat_id,)) | |||||
| media_messages = cursor.fetchall() | |||||
| conn.close() | |||||
| if not media_messages: | |||||
| return # No media to display | |||||
| # Sanitize contact_jid for filename | |||||
| if contact_jid: | |||||
| safe_filename = "".join(c if c.isalnum() else "_" for c in contact_jid) | |||||
| else: | |||||
| safe_filename = str(chat_id) | |||||
| media_dir = os.path.join(output_dir, "media") | |||||
| os.makedirs(media_dir, exist_ok=True) | |||||
| media_gallery_path = os.path.join(media_dir, f"{safe_filename}.html") | |||||
| with open(media_gallery_path, 'w', encoding='utf-8') as f: | |||||
| f.write(f""" | |||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||||
| <title>Media from {html.escape(str(chat_name))}</title> | |||||
| <style> | |||||
| body {{ | |||||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |||||
| background-color: #f4f4f9; | |||||
| margin: 0; | |||||
| padding: 20px; | |||||
| min-height: 100vh; | |||||
| }} | |||||
| .header {{ | |||||
| background-color: #128C7E; | |||||
| color: white; | |||||
| padding: 20px; | |||||
| margin: -20px -20px 20px -20px; | |||||
| text-align: center; | |||||
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |||||
| }} | |||||
| .header h1 {{ | |||||
| margin: 0; | |||||
| font-size: 1.8em; | |||||
| }} | |||||
| .nav-links {{ | |||||
| margin-top: 10px; | |||||
| }} | |||||
| .nav-links a {{ | |||||
| color: rgba(255,255,255,0.9); | |||||
| text-decoration: none; | |||||
| margin: 0 10px; | |||||
| padding: 5px 10px; | |||||
| border-radius: 5px; | |||||
| transition: background-color 0.2s; | |||||
| }} | |||||
| .nav-links a:hover {{ | |||||
| background-color: rgba(255,255,255,0.1); | |||||
| }} | |||||
| .container {{ | |||||
| max-width: 1200px; | |||||
| margin: auto; | |||||
| background: white; | |||||
| padding: 20px; | |||||
| border-radius: 12px; | |||||
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |||||
| }} | |||||
| .media-grid {{ | |||||
| display: grid; | |||||
| grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | |||||
| gap: 15px; | |||||
| margin-top: 20px; | |||||
| }} | |||||
| .media-item {{ | |||||
| background: #f8f9fa; | |||||
| border-radius: 12px; | |||||
| padding: 10px; | |||||
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |||||
| transition: transform 0.2s; | |||||
| }} | |||||
| .media-item:hover {{ | |||||
| transform: translateY(-2px); | |||||
| }} | |||||
| .media-header {{ | |||||
| display: flex; | |||||
| align-items: center; | |||||
| margin-bottom: 8px; | |||||
| font-size: 0.85em; | |||||
| color: #666; | |||||
| }} | |||||
| .sender-name {{ | |||||
| font-weight: 500; | |||||
| color: #005c4b; | |||||
| }} | |||||
| .timestamp {{ | |||||
| margin-left: auto; | |||||
| font-size: 0.8em; | |||||
| color: #999; | |||||
| }} | |||||
| .gallery-media {{ | |||||
| width: 100%; | |||||
| max-height: 200px; | |||||
| object-fit: cover; | |||||
| border-radius: 8px; | |||||
| cursor: pointer; | |||||
| }} | |||||
| .raw-file-link {{ | |||||
| display: inline-block; | |||||
| margin-top: 8px; | |||||
| padding: 4px 8px; | |||||
| background: #128C7E; | |||||
| color: white; | |||||
| text-decoration: none; | |||||
| border-radius: 4px; | |||||
| font-size: 0.75em; | |||||
| transition: background-color 0.2s; | |||||
| }} | |||||
| .raw-file-link:hover {{ | |||||
| background: #0d6b5e; | |||||
| color: white; | |||||
| text-decoration: none; | |||||
| }} | |||||
| .file-media {{ | |||||
| padding: 15px; | |||||
| text-align: center; | |||||
| background: #e9ecef; | |||||
| border-radius: 8px; | |||||
| border: 2px dashed #adb5bd; | |||||
| }} | |||||
| .file-media a {{ | |||||
| text-decoration: none; | |||||
| color: #495057; | |||||
| font-weight: 500; | |||||
| }} | |||||
| </style> | |||||
| </head> | |||||
| <body> | |||||
| <div class="header"> | |||||
| <h1>📷 Media from {html.escape(str(chat_name))}</h1> | |||||
| <div class="nav-links"> | |||||
| <a href="../chats/{safe_filename}.html">← Back to Chat</a> | |||||
| <a href="../media-gallery.html">🖼️ All Media</a> | |||||
| <a href="../whatsapp-chats.html">💬 All Chats</a> | |||||
| </div> | |||||
| </div> | |||||
| <div class="container"> | |||||
| <p>{len(media_messages)} media files in this chat, sorted by date (newest first).</p> | |||||
| <div class="media-grid"> | |||||
| """) | |||||
| for message_date, media_path, is_from_me, from_jid, group_member_name, push_name, group_member_jid, sender_partner_name, group_member_name_fallback in media_messages: | |||||
| if not media_path: | |||||
| continue | |||||
| # Determine sender name | |||||
| if is_from_me: | |||||
| sender_name = "You" | |||||
| else: | |||||
| # For group messages, prioritize ZCONTACTNAME from ZWAGROUPMEMBER linked via ZGROUPMEMBER | |||||
| sender_name = group_member_name or group_member_name_fallback # Try direct link first, then fallback via ZFROMJID | |||||
| if not sender_name: | |||||
| # Try sender's partner name from their individual chat session | |||||
| sender_name = sender_partner_name or push_name | |||||
| if not sender_name: | |||||
| # Check if this is a group chat and ZFROMJID is the group JID (can't determine individual sender) | |||||
| if contact_jid and '@g.us' in contact_jid and from_jid and '@g.us' in from_jid: | |||||
| sender_name = "Group Member" # Generic fallback for unidentifiable group messages | |||||
| elif group_member_jid and '@' in group_member_jid: | |||||
| phone_number = group_member_jid.split('@')[0] | |||||
| sender_name = f"+{phone_number}" if phone_number.isdigit() else group_member_jid | |||||
| elif from_jid and '@' in from_jid: | |||||
| phone_number = from_jid.split('@')[0] | |||||
| sender_name = f"+{phone_number}" if phone_number.isdigit() else from_jid | |||||
| else: | |||||
| sender_name = "Unknown" | |||||
| # Generate media HTML with proper relative path | |||||
| media_html = get_media_tag_for_gallery(media_path, output_dir, "../") | |||||
| if not media_html: | |||||
| continue | |||||
| f.write(f""" | |||||
| <div class="media-item"> | |||||
| <div class="media-header"> | |||||
| <span class="sender-name">{html.escape(str(sender_name))}</span> | |||||
| <span class="timestamp">{convert_whatsapp_timestamp(message_date)}</span> | |||||
| </div> | |||||
| {media_html} | |||||
| <a href="../Message/{media_path}" target="_blank" class="raw-file-link">📁 Open File</a> | |||||
| </div> | |||||
| """) | |||||
| f.write(""" | |||||
| </div> | |||||
| </div> | |||||
| </body> | |||||
| </html> | |||||
| """) | |||||
| print(f"Generated chat media gallery for: {chat_name}") | |||||
| def get_media_tag(media_path, output_dir): | def get_media_tag(media_path, output_dir): | ||||
| """Generates the appropriate HTML tag for a given media file and copies it.""" | """Generates the appropriate HTML tag for a given media file and copies it.""" | ||||
| if not media_path: | if not media_path: | ||||
| @@ -53,28 +707,39 @@ def generate_html_chat(db_path, media_path, output_dir, chat_id, chat_name, is_g | |||||
| conn = sqlite3.connect(db_path) | conn = sqlite3.connect(db_path) | ||||
| cursor = conn.cursor() | cursor = conn.cursor() | ||||
| # Updated query to fetch more potential name fields (like ZFIRSTNAME) to find the best one. | |||||
| # Updated query to fetch more potential name fields and properly resolve group member names | |||||
| query = """ | query = """ | ||||
| SELECT | SELECT | ||||
| m.ZISFROMME, | m.ZISFROMME, | ||||
| m.ZTEXT, | m.ZTEXT, | ||||
| m.ZMESSAGEDATE, | m.ZMESSAGEDATE, | ||||
| m.ZFROMJID, | m.ZFROMJID, | ||||
| g.ZCONTACTNAME AS GroupMemberContactName, | |||||
| gm_p.ZPUSHNAME AS GroupMemberContactName, | |||||
| cs.ZPARTNERNAME AS ChatPartnerName, | cs.ZPARTNERNAME AS ChatPartnerName, | ||||
| p.ZPUSHNAME AS ProfilePushName, | p.ZPUSHNAME AS ProfilePushName, | ||||
| mi.ZMEDIALOCALPATH, | mi.ZMEDIALOCALPATH, | ||||
| cs.ZCONTACTJID AS ChatJID | |||||
| cs.ZCONTACTJID AS ChatJID, | |||||
| gm.ZMEMBERJID AS GroupMemberJID, | |||||
| sender_cs.ZPARTNERNAME AS SenderPartnerName, | |||||
| gm_fallback_p.ZPUSHNAME AS GroupMemberContactNameFallback | |||||
| FROM | FROM | ||||
| ZWAMESSAGE m | ZWAMESSAGE m | ||||
| LEFT JOIN | |||||
| ZWAGROUPMEMBER g ON m.ZGROUPMEMBER = g.Z_PK | |||||
| LEFT JOIN | LEFT JOIN | ||||
| ZWACHATSESSION cs ON m.ZCHATSESSION = cs.Z_PK | ZWACHATSESSION cs ON m.ZCHATSESSION = cs.Z_PK | ||||
| LEFT JOIN | |||||
| ZWAGROUPMEMBER gm ON gm.Z_PK = m.ZGROUPMEMBER | |||||
| LEFT JOIN | |||||
| ZWAPROFILEPUSHNAME gm_p ON gm.ZMEMBERJID = gm_p.ZJID | |||||
| LEFT JOIN | LEFT JOIN | ||||
| ZWAPROFILEPUSHNAME p ON m.ZFROMJID = p.ZJID | ZWAPROFILEPUSHNAME p ON m.ZFROMJID = p.ZJID | ||||
| LEFT JOIN | LEFT JOIN | ||||
| ZWAMEDIAITEM mi ON m.ZMEDIAITEM = mi.Z_PK | ZWAMEDIAITEM mi ON m.ZMEDIAITEM = mi.Z_PK | ||||
| LEFT JOIN | |||||
| ZWACHATSESSION sender_cs ON sender_cs.ZCONTACTJID = m.ZFROMJID | |||||
| LEFT JOIN | |||||
| ZWAGROUPMEMBER gm_fallback ON gm_fallback.ZCHATSESSION = m.ZCHATSESSION AND gm_fallback.ZMEMBERJID = m.ZFROMJID | |||||
| LEFT JOIN | |||||
| ZWAPROFILEPUSHNAME gm_fallback_p ON gm_fallback.ZMEMBERJID = gm_fallback_p.ZJID | |||||
| WHERE | WHERE | ||||
| m.ZCHATSESSION = ? | m.ZCHATSESSION = ? | ||||
| ORDER BY | ORDER BY | ||||
| @@ -136,6 +801,21 @@ def generate_html_chat(db_path, media_path, output_dir, chat_id, chat_name, is_g | |||||
| font-size: 1.2em; | font-size: 1.2em; | ||||
| text-align: center; | text-align: center; | ||||
| }} | }} | ||||
| .nav-links {{ | |||||
| margin-top: 8px; | |||||
| font-size: 0.8em; | |||||
| }} | |||||
| .nav-links a {{ | |||||
| color: rgba(255,255,255,0.9); | |||||
| text-decoration: none; | |||||
| margin: 0 8px; | |||||
| padding: 3px 8px; | |||||
| border-radius: 3px; | |||||
| transition: background-color 0.2s; | |||||
| }} | |||||
| .nav-links a:hover {{ | |||||
| background-color: rgba(255,255,255,0.1); | |||||
| }} | |||||
| .chat-header-id {{ | .chat-header-id {{ | ||||
| @@ -200,12 +880,16 @@ def generate_html_chat(db_path, media_path, output_dir, chat_id, chat_name, is_g | |||||
| <div class="chat-header"> | <div class="chat-header"> | ||||
| {html.escape(chat_name)} | {html.escape(chat_name)} | ||||
| <div class="chat-header-id">{contact_jid}</div> | <div class="chat-header-id">{contact_jid}</div> | ||||
| <div class="nav-links"> | |||||
| <a href="../whatsapp-chats.html">← Back</a> | |||||
| <a href="../media/{safe_filename}.html">📷 Media</a> | |||||
| </div> | |||||
| </div> | </div> | ||||
| <div class="chat-box"> | <div class="chat-box"> | ||||
| """) | """) | ||||
| # Write messages | # Write messages | ||||
| for is_from_me, text, timestamp, from_jid, group_member_contact_name, chat_partner_name, profile_push_name, media_local_path, contact_jid in messages: | |||||
| for is_from_me, text, timestamp, from_jid, group_member_contact_name, chat_partner_name, profile_push_name, media_local_path, contact_jid, group_member_jid, sender_partner_name, group_member_contact_name_fallback in messages: | |||||
| msg_class = "sent" if is_from_me else "received" | msg_class = "sent" if is_from_me else "received" | ||||
| f.write(f'<div class="message {msg_class}">') | f.write(f'<div class="message {msg_class}">') | ||||
| @@ -214,20 +898,25 @@ def generate_html_chat(db_path, media_path, output_dir, chat_id, chat_name, is_g | |||||
| if not is_from_me: | if not is_from_me: | ||||
| # Prioritize group member contact name for group chats | # Prioritize group member contact name for group chats | ||||
| if is_group: | if is_group: | ||||
| # Try names in order of preference, avoiding encoded-looking strings | |||||
| potential_names = [ | |||||
| group_member_contact_name, | |||||
| profile_push_name, | |||||
| from_jid, | |||||
| chat_partner_name, | |||||
| ] | |||||
| # For group messages, prioritize ZCONTACTNAME from ZWAGROUPMEMBER linked via ZGROUPMEMBER | |||||
| sender_name = group_member_contact_name or group_member_contact_name_fallback # Try direct link first, then fallback via ZFROMJID | |||||
| # Filter out None values and strings that look like they're encoded | |||||
| valid_names = [name for name in potential_names if name and not ( | |||||
| name.startswith('CK') and any(c.isupper() for c in name[2:]) and '=' in name | |||||
| )] | |||||
| sender_name = next((name for name in valid_names), "Unknown") | |||||
| if not sender_name: | |||||
| # Try sender's partner name from their individual chat session | |||||
| sender_name = sender_partner_name or profile_push_name | |||||
| if not sender_name: | |||||
| # Check if this is a group chat and ZFROMJID is the group JID (can't determine individual sender) | |||||
| if contact_jid and '@g.us' in contact_jid and from_jid and '@g.us' in from_jid: | |||||
| sender_name = "Group Member" # Generic fallback for unidentifiable group messages | |||||
| elif group_member_jid and '@' in group_member_jid: | |||||
| phone_number = group_member_jid.split('@')[0] | |||||
| sender_name = f"+{phone_number}" if phone_number.isdigit() else group_member_jid | |||||
| elif from_jid and '@' in from_jid: | |||||
| phone_number = from_jid.split('@')[0] | |||||
| sender_name = f"+{phone_number}" if phone_number.isdigit() else from_jid | |||||
| else: | |||||
| sender_name = "Unknown" | |||||
| else: | else: | ||||
| # For individual chats, prefer partner name or push name | # For individual chats, prefer partner name or push name | ||||
| sender_name = chat_partner_name or profile_push_name or from_jid or "Unknown" | sender_name = chat_partner_name or profile_push_name or from_jid or "Unknown" | ||||
| @@ -298,7 +987,7 @@ def process_iphone_backup(backup_path, output_dir): | |||||
| os.makedirs(os.path.dirname(dest_file), exist_ok=True) | os.makedirs(os.path.dirname(dest_file), exist_ok=True) | ||||
| if not os.path.exists(src_file): | if not os.path.exists(src_file): | ||||
| print(f"Source file missing: {src_file}") | |||||
| # print(f"Source file missing: {src_file}") | |||||
| skipped_files += 1 | skipped_files += 1 | ||||
| continue | continue | ||||
| @@ -668,6 +1357,9 @@ def main(): | |||||
| <div class="header"> | <div class="header"> | ||||
| <h1>WhatsApp Chat Export</h1> | <h1>WhatsApp Chat Export</h1> | ||||
| <div class="export-info">Exported on {datetime.now().strftime('%Y-%m-%d %H:%M')}</div> | <div class="export-info">Exported on {datetime.now().strftime('%Y-%m-%d %H:%M')}</div> | ||||
| <div style="margin-top: 10px;"> | |||||
| <a href="media-gallery.html" style="color: rgba(255,255,255,0.9); text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: rgba(255,255,255,0.1);">📷 View All Media</a> | |||||
| </div> | |||||
| </div> | </div> | ||||
| <div class="container"> | <div class="container"> | ||||
| <ul> | <ul> | ||||
| @@ -720,6 +1412,9 @@ def main(): | |||||
| if message_count > 0: | if message_count > 0: | ||||
| # Generate chat HTML only for chats with messages | # Generate chat HTML only for chats with messages | ||||
| generate_html_chat(db_path, media_path, args.output, chat_id, chat_name, is_group, contact_jid) | generate_html_chat(db_path, media_path, args.output, chat_id, chat_name, is_group, contact_jid) | ||||
| # Generate individual chat media gallery | |||||
| generate_chat_media_gallery(db_path, args.output, chat_id, chat_name, contact_jid) | |||||
| # Clickable entry with link | # Clickable entry with link | ||||
| index_f.write( | index_f.write( | ||||
| @@ -747,6 +1442,9 @@ def main(): | |||||
| index_f.write("</ul></div></body></html>") | index_f.write("</ul></div></body></html>") | ||||
| # Generate the all-media gallery | |||||
| generate_all_media_gallery(db_path, args.output) | |||||
| # Create a simple redirect index.html | # Create a simple redirect index.html | ||||
| redirect_index = os.path.join(args.output, "index.html") | redirect_index = os.path.join(args.output, "index.html") | ||||
| with open(redirect_index, 'w', encoding='utf-8') as f: | with open(redirect_index, 'w', encoding='utf-8') as f: | ||||
| @@ -765,6 +1463,9 @@ def main(): | |||||
| print(f"View your chats by opening either of these files in your browser:") | print(f"View your chats by opening either of these files in your browser:") | ||||
| print(f" • {os.path.abspath(index_path)}") | print(f" • {os.path.abspath(index_path)}") | ||||
| print(f" • {os.path.abspath(redirect_index)}") | print(f" • {os.path.abspath(redirect_index)}") | ||||
| print(f"\nAdditional features:") | |||||
| print(f" • Media Gallery: {os.path.abspath(os.path.join(args.output, 'media-gallery.html'))}") | |||||
| print(f" • Individual chat media galleries available in the media/ folder") | |||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||