|
|
@@ -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__": |
|
|
|