Browse Source

Add media gallery generation and improve HTML output for chats

main
Andreas Demmelbauer 1 week ago
parent
commit
5302708f73
1 changed files with 721 additions and 20 deletions
  1. +721
    -20
      whatsapp_archiver.py

+ 721
- 20
whatsapp_archiver.py View File

@@ -22,6 +22,660 @@ def convert_whatsapp_timestamp(ts):
except (ValueError, TypeError):
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):
"""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
<div class="chat-header">
{html.escape(chat_name)}
<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 class="chat-box">
""")

# 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'<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:
# 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():
<div class="header">
<h1>WhatsApp Chat Export</h1>
<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 class="container">
<ul>
@@ -720,6 +1412,9 @@ def main():
if message_count > 0:
# 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 individual chat media gallery
generate_chat_media_gallery(db_path, args.output, chat_id, chat_name, contact_jid)

# Clickable entry with link
index_f.write(
@@ -747,6 +1442,9 @@ def main():

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
redirect_index = os.path.join(args.output, "index.html")
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" • {os.path.abspath(index_path)}")
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__":


Loading…
Cancel
Save