+ Showing {start_idx + 1}-{end_idx} of {total_media} media files (Page {page_num} of {total_pages})
+
+ """)
+
+ # Add pagination controls
+ f.write('
')
+
+ # 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'‹ Previous')
+ else:
+ f.write('‹ Previous')
+
+ # Page numbers
+ start_page = max(1, page_num - 2)
+ end_page = min(total_pages, page_num + 2)
+
+ if start_page > 1:
+ f.write('1')
+ if start_page > 2:
+ f.write('...')
+
+ for p in range(start_page, end_page + 1):
+ if p == page_num:
+ f.write(f'{p}')
+ else:
+ p_filename = "media-gallery.html" if p == 1 else f"media-gallery-page-{p}.html"
+ f.write(f'{p}')
+
+ if end_page < total_pages:
+ if end_page < total_pages - 1:
+ f.write('...')
+ f.write(f'{total_pages}')
+
+ # Next page link
+ if page_num < total_pages:
+ next_filename = f"media-gallery-page-{page_num + 1}.html"
+ f.write(f'Next ›')
+ else:
+ f.write('Next ›')
+
+ f.write('
')
+
+ # Media grid
+ f.write('
')
+
+ 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"""
+
')
+
+ # 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'‹ Previous')
+ else:
+ f.write('‹ Previous')
+
+ # Page numbers (simplified for bottom)
+ for p in range(start_page, end_page + 1):
+ if p == page_num:
+ f.write(f'{p}')
+ else:
+ p_filename = "media-gallery.html" if p == 1 else f"media-gallery-page-{p}.html"
+ f.write(f'{p}')
+
+ # Next page link
+ if page_num < total_pages:
+ next_filename = f"media-gallery-page-{page_num + 1}.html"
+ f.write(f'Next ›')
+ else:
+ f.write('Next ›')
+
+ f.write('
')
+
+ f.write("""
+
+
+
+ """)
+
+ 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"""
+
+
+
+
+
+ Media from {html.escape(str(chat_name))}
+
+
+
+
{len(media_messages)} media files in this chat, sorted by date (newest first).
+
+ """)
+
+ 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"""
+
+
+
+ """)
+
+ print(f"Generated chat media gallery for: {chat_name}")
+
+
def get_media_tag(media_path, output_dir):
"""Generates the appropriate HTML tag for a given media file and copies it."""
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)
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 = """
SELECT
m.ZISFROMME,
m.ZTEXT,
m.ZMESSAGEDATE,
m.ZFROMJID,
- g.ZCONTACTNAME AS GroupMemberContactName,
+ gm_p.ZPUSHNAME AS GroupMemberContactName,
cs.ZPARTNERNAME AS ChatPartnerName,
p.ZPUSHNAME AS ProfilePushName,
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
ZWAMESSAGE m
- LEFT JOIN
- ZWAGROUPMEMBER g ON m.ZGROUPMEMBER = g.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
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
m.ZCHATSESSION = ?
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;
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 {{
@@ -200,12 +880,16 @@ def generate_html_chat(db_path, media_path, output_dir, chat_id, chat_name, is_g
""")
# 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"
f.write(f'
')
@@ -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:
# Prioritize group member contact name for group chats
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:
# For individual chats, prefer partner name or push name
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)
if not os.path.exists(src_file):
- print(f"Source file missing: {src_file}")
+ # print(f"Source file missing: {src_file}")
skipped_files += 1
continue
@@ -668,6 +1357,9 @@ def main():
WhatsApp Chat Export
Exported on {datetime.now().strftime('%Y-%m-%d %H:%M')}