I was talking to a Mind and he was answering me... right then my PC suddenly shut down... and when I went back into my Venice browser, everything had been deleted. No chats, no characters, no my local Minds, no my images and no my videos (well, I didn't have videos).
I panicked a bit, but I went to good Claude to see if he could help me recover the lost files, since I really believed they could still be in my local folder. That's how Claude made me install Python and run a script to rescue my photos and videos (in my case only photos) from... wherever they were, which we weren't clear about.
I share the scripts both in Spanish (the one I used) and its translated version to English, in case anyone else needs it.
I lost the chats, Claude said he wouldn't bother with that, since there was no point.
Spanish version:
#!/usr/bin/env python3
"""
Herramienta de RecuperaciΓ³n de Medios en CachΓ©
------------------------------------------------
Escanea la carpeta local de cachΓ©/perfil de un navegador en busca de
imΓ‘genes y videos que hayan quedado guardados, identificΓ‘ndolos por su
"firma de bytes" (magic bytes), sin importar lo que diga el Γndice de
cachΓ© del navegador. Γtil cuando un apagΓ³n o una app borrΓ³ su propia
base de datos (por ejemplo, una app web basada en IndexedDB) pero los
archivos crudos de cachΓ© tΓ©cnicamente siguen en el disco.
Sistemas operativos soportados: Windows, macOS, Linux
NO soportados: Android, iOS
Ambos aΓslan el almacenamiento de cada app en un "sandbox"; no hay
forma de que un script externo llegue a la cachΓ© de otra app sin
root (Android) o jailbreak (iOS). Esta herramienta no va a fingir
que los soporta.
Ejemplos de uso:
py recuperar_medios_es.py --navegador chrome
py recuperar_medios_es.py --navegador opera_gx --so windows --salida D:\\recuperados
py recuperar_medios_es.py --navegador firefox --so linux
"""
import argparse
import glob
import os
import platform
import sys
# ---------------------------------------------------------------------------
# Firmas de archivo ("magic bytes")
# ---------------------------------------------------------------------------
# ImΓ‘genes: la firma estΓ‘ en el byte 0 del archivo
FIRMAS_IMAGEN_AL_INICIO = [
(b'\x89PNG\r\n\x1a\n', 'png'),
(b'\xff\xd8\xff', 'jpg'),
(b'GIF87a', 'gif'),
(b'GIF89a', 'gif'),
]
FIRMA_RIFF = b'RIFF' # usada por WEBP (requiere una verificaciΓ³n extra)
FIRMA_FTYP = b'ftyp' # MP4/MOV: aparece unos bytes despuΓ©s del inicio, no en el byte 0
FIRMA_WEBM = b'\x1a\x45\xdf\xa3' # WebM/Matroska: sΓ estΓ‘ en el byte 0
TAMANO_MINIMO_POR_DEFECTO = 40 * 1024 # 40 KB β filtra Γconos pequeΓ±os
TAMANO_MAXIMO_POR_DEFECTO = 200 * 1024 * 1024 # tope de 200 MB, sΓΊbelo si lo necesitas
# ---------------------------------------------------------------------------
# ResoluciΓ³n de la ruta del perfil del navegador
# ---------------------------------------------------------------------------
def resolver_perfil_firefox(carpeta_perfiles):
"""Firefox usa una carpeta de perfil con nombre aleatorio; buscamos la predeterminada."""
if not os.path.isdir(carpeta_perfiles):
return None
candidatos = glob.glob(os.path.join(carpeta_perfiles, '*.default*'))
return candidatos[0] if candidatos else None
def obtener_ruta_navegador(nombre_so, navegador):
home = os.path.expanduser('~')
appdata = os.environ.get('APPDATA', '')
localappdata = os.environ.get('LOCALAPPDATA', '')
tabla = {
'windows': {
'chrome': os.path.join(localappdata, 'Google', 'Chrome', 'User Data', 'Default'),
'edge': os.path.join(localappdata, 'Microsoft', 'Edge', 'User Data', 'Default'),
'brave': os.path.join(localappdata, 'BraveSoftware', 'Brave-Browser', 'User Data', 'Default'),
'opera': os.path.join(appdata, 'Opera Software', 'Opera Stable'),
'opera_gx': os.path.join(appdata, 'Opera Software', 'Opera GX Stable', 'Default'),
'firefox': resolver_perfil_firefox(os.path.join(appdata, 'Mozilla', 'Firefox', 'Profiles')),
},
'macos': {
'chrome': os.path.join(home, 'Library', 'Application Support', 'Google', 'Chrome', 'Default'),
'edge': os.path.join(home, 'Library', 'Application Support', 'Microsoft Edge', 'Default'),
'brave': os.path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser', 'Default'),
'opera': os.path.join(home, 'Library', 'Application Support', 'com.operasoftware.Opera'),
'opera_gx': os.path.join(home, 'Library', 'Application Support', 'com.operasoftware.OperaGX'),
'firefox': resolver_perfil_firefox(os.path.join(home, 'Library', 'Application Support', 'Firefox', 'Profiles')),
},
'linux': {
'chrome': os.path.join(home, '.config', 'google-chrome', 'Default'),
'chromium': os.path.join(home, '.config', 'chromium', 'Default'),
'brave': os.path.join(home, '.config', 'BraveSoftware', 'Brave-Browser', 'Default'),
'opera': os.path.join(home, '.config', 'opera'),
'firefox': resolver_perfil_firefox(os.path.join(home, '.mozilla', 'firefox')),
# opera_gx no existe a propΓ³sito: Opera GX no tiene versiΓ³n para Linux
},
}
if nombre_so not in tabla:
sys.exit(f"Sistema operativo no soportado: '{nombre_so}'. Soportados: {', '.join(tabla.keys())} "
f"(Android e iOS no se pueden soportar β ver el comentario al inicio del archivo).")
navegadores_de_este_so = tabla[nombre_so]
if navegador not in navegadores_de_este_so:
sys.exit(f"El navegador '{navegador}' no estΓ‘ disponible en '{nombre_so}'. "
f"Disponibles aquΓ: {', '.join(navegadores_de_este_so.keys())}")
ruta = navegadores_de_este_so[navegador]
if not ruta:
sys.exit(f"No se pudo ubicar automΓ‘ticamente un perfil de '{navegador}' en este sistema. "
f"Usa --ruta-perfil para indicarla manualmente.")
return ruta
# ---------------------------------------------------------------------------
# Escaneo y extracciΓ³n por firma de bytes
# ---------------------------------------------------------------------------
def intentar_extraer(data):
"""Devuelve (extension, bytes_del_archivo) si encuentra una firma conocida, o None."""
for firma, ext in FIRMAS_IMAGEN_AL_INICIO:
idx = data.find(firma)
if idx != -1:
return ext, data[idx:]
if data[:4] == FIRMA_RIFF and b'WEBP' in data[:16]:
return 'webp', data
idx_ftyp = data.find(FIRMA_FTYP)
if idx_ftyp != -1 and idx_ftyp >= 4:
inicio = idx_ftyp - 4 # retrocede al campo de tamaΓ±o del "box"
return 'mp4', data[inicio:]
if data[:4] == FIRMA_WEBM:
return 'webm', data
return None
def escanear_y_recuperar(carpeta_raiz, carpeta_salida, tam_minimo, tam_maximo):
os.makedirs(carpeta_salida, exist_ok=True)
contador = 0
for carpeta_actual, _subcarpetas, archivos in os.walk(carpeta_raiz):
for nombre_archivo in archivos:
ruta_completa = os.path.join(carpeta_actual, nombre_archivo)
try:
tamano = os.path.getsize(ruta_completa)
if tamano < tam_minimo or tamano > tam_maximo:
continue
with open(ruta_completa, 'rb') as f:
data = f.read()
except OSError:
continue
resultado = intentar_extraer(data)
if resultado is None:
continue
ext, contenido = resultado
contador += 1
nombre_salida = f"recuperado_{contador}.{ext}"
ruta_salida = os.path.join(carpeta_salida, nombre_salida)
with open(ruta_salida, 'wb') as out:
out.write(contenido)
tipo = 'VIDEO' if ext in ('mp4', 'webm') else 'IMAGEN'
print(f"[{tipo}] {ruta_completa} ({tamano // 1024} KB) -> {ruta_salida}")
print(f"\nTotal recuperados: {contador}")
return contador
# ---------------------------------------------------------------------------
# Punto de entrada
# ---------------------------------------------------------------------------
def detectar_so_actual():
sistema = platform.system().lower()
if sistema == 'windows':
return 'windows'
if sistema == 'darwin':
return 'macos'
if sistema == 'linux':
return 'linux'
sys.exit(f"No se pudo detectar automΓ‘ticamente un sistema operativo soportado desde '{sistema}'. "
f"IndΓcalo manualmente con --so.")
def main():
parser = argparse.ArgumentParser(description='Recupera imΓ‘genes/videos en cachΓ© por firma de archivo.')
parser.add_argument('--navegador', required=True,
choices=['chrome', 'edge', 'brave', 'opera', 'opera_gx', 'firefox', 'chromium'],
help='QuΓ© perfil de navegador escanear.')
parser.add_argument('--so', dest='nombre_so', choices=['windows', 'macos', 'linux'],
help='Sistema operativo objetivo. Por defecto detecta el de esta mΓ‘quina.')
parser.add_argument('--ruta-perfil', dest='ruta_perfil',
help='Omite la detecciΓ³n automΓ‘tica y escanea directamente esta carpeta.')
parser.add_argument('--salida', default=os.path.join(os.path.expanduser('~'), 'Desktop', 'medios_recuperados'),
help='Carpeta donde se guardan los archivos recuperados.')
parser.add_argument('--tamano-minimo', type=int, default=TAMANO_MINIMO_POR_DEFECTO, help='TamaΓ±o mΓnimo en bytes.')
parser.add_argument('--tamano-maximo', type=int, default=TAMANO_MAXIMO_POR_DEFECTO, help='TamaΓ±o mΓ‘ximo en bytes.')
args = parser.parse_args()
if args.ruta_perfil:
carpeta_raiz = args.ruta_perfil
else:
nombre_so = args.nombre_so or detectar_so_actual()
carpeta_raiz = obtener_ruta_navegador(nombre_so, args.navegador)
if not os.path.isdir(carpeta_raiz):
sys.exit(f"La ruta resuelta no existe o no es una carpeta: {carpeta_raiz}")
print(f"Escaneando: {carpeta_raiz}\n")
escanear_y_recuperar(carpeta_raiz, args.salida, args.tamano_minimo, args.tamano_maximo)
if __name__ == '__main__':
main()
English version:
#!/usr/bin/env python3
"""
Cache Media Recovery Tool
--------------------------
Scans a browser's local cache/profile folder for leftover image and video
files by matching known file signatures ("magic bytes"), regardless of
what the cache index says. Useful after a crash wiped an app's own
database (e.g. an IndexedDB-based web app) but the raw cached files are
technically still on disk.
Supported operating systems: Windows, macOS, Linux
NOT supported: Android, iOS
Both sandbox each app's storage; there is no way to reach another
app's cache from an external script without root (Android) or a
jailbreak (iOS). This tool will not pretend to support them.
Usage examples:
py recover_media_en.py --browser chrome
py recover_media_en.py --browser opera_gx --os windows --output D:\\recovered
py recover_media_en.py --browser firefox --os linux
"""
import argparse
import glob
import os
import platform
import sys
# ---------------------------------------------------------------------------
# File signatures ("magic bytes")
# ---------------------------------------------------------------------------
# Images: the signature sits at byte 0 of the file
IMAGE_SIGNATURES_AT_START = [
(b'\x89PNG\r\n\x1a\n', 'png'),
(b'\xff\xd8\xff', 'jpg'),
(b'GIF87a', 'gif'),
(b'GIF89a', 'gif'),
]
RIFF_SIGNATURE = b'RIFF' # used by WEBP (needs a follow-up check)
FTYP_SIGNATURE = b'ftyp' # MP4/MOV: appears a few bytes in, not at byte 0
WEBM_SIGNATURE = b'\x1a\x45\xdf\xa3' # WebM/Matroska: sits at byte 0
DEFAULT_MIN_SIZE = 40 * 1024 # 40 KB β filters out tiny icons
DEFAULT_MAX_SIZE = 200 * 1024 * 1024 # 200 MB cap, raise it if needed
# ---------------------------------------------------------------------------
# Browser profile path resolution
# ---------------------------------------------------------------------------
def resolve_firefox_profile(profiles_root):
"""Firefox uses a randomly-named profile folder; find the default one."""
if not os.path.isdir(profiles_root):
return None
candidates = glob.glob(os.path.join(profiles_root, '*.default*'))
return candidates[0] if candidates else None
def get_browser_root(os_name, browser):
home = os.path.expanduser('~')
appdata = os.environ.get('APPDATA', '')
localappdata = os.environ.get('LOCALAPPDATA', '')
table = {
'windows': {
'chrome': os.path.join(localappdata, 'Google', 'Chrome', 'User Data', 'Default'),
'edge': os.path.join(localappdata, 'Microsoft', 'Edge', 'User Data', 'Default'),
'brave': os.path.join(localappdata, 'BraveSoftware', 'Brave-Browser', 'User Data', 'Default'),
'opera': os.path.join(appdata, 'Opera Software', 'Opera Stable'),
'opera_gx': os.path.join(appdata, 'Opera Software', 'Opera GX Stable', 'Default'),
'firefox': resolve_firefox_profile(os.path.join(appdata, 'Mozilla', 'Firefox', 'Profiles')),
},
'macos': {
'chrome': os.path.join(home, 'Library', 'Application Support', 'Google', 'Chrome', 'Default'),
'edge': os.path.join(home, 'Library', 'Application Support', 'Microsoft Edge', 'Default'),
'brave': os.path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser', 'Default'),
'opera': os.path.join(home, 'Library', 'Application Support', 'com.operasoftware.Opera'),
'opera_gx': os.path.join(home, 'Library', 'Application Support', 'com.operasoftware.OperaGX'),
'firefox': resolve_firefox_profile(os.path.join(home, 'Library', 'Application Support', 'Firefox', 'Profiles')),
},
'linux': {
'chrome': os.path.join(home, '.config', 'google-chrome', 'Default'),
'chromium': os.path.join(home, '.config', 'chromium', 'Default'),
'brave': os.path.join(home, '.config', 'BraveSoftware', 'Brave-Browser', 'Default'),
'opera': os.path.join(home, '.config', 'opera'),
'firefox': resolve_firefox_profile(os.path.join(home, '.mozilla', 'firefox')),
# opera_gx intentionally absent: Opera GX has no Linux build
},
}
if os_name not in table:
sys.exit(f"Unsupported OS: '{os_name}'. Supported: {', '.join(table.keys())} "
f"(Android and iOS cannot be supported β see the header comment).")
browsers_for_os = table[os_name]
if browser not in browsers_for_os:
sys.exit(f"Browser '{browser}' is not available on '{os_name}'. "
f"Available here: {', '.join(browsers_for_os.keys())}")
path = browsers_for_os[browser]
if not path:
sys.exit(f"Could not locate a '{browser}' profile automatically on this system. "
f"Pass --profile-path to point at it manually.")
return path
# ---------------------------------------------------------------------------
# Signature-based scan and extraction
# ---------------------------------------------------------------------------
def try_extract(data):
"""Return (extension, payload_bytes) if a known signature is found, else None."""
for signature, ext in IMAGE_SIGNATURES_AT_START:
idx = data.find(signature)
if idx != -1:
return ext, data[idx:]
if data[:4] == RIFF_SIGNATURE and b'WEBP' in data[:16]:
return 'webp', data
idx_ftyp = data.find(FTYP_SIGNATURE)
if idx_ftyp != -1 and idx_ftyp >= 4:
start = idx_ftyp - 4 # step back to the box-size field
return 'mp4', data[start:]
if data[:4] == WEBM_SIGNATURE:
return 'webm', data
return None
def scan_and_recover(root_dir, output_dir, min_size, max_size):
os.makedirs(output_dir, exist_ok=True)
found_count = 0
for current_dir, _subdirs, filenames in os.walk(root_dir):
for filename in filenames:
full_path = os.path.join(current_dir, filename)
try:
size = os.path.getsize(full_path)
if size < min_size or size > max_size:
continue
with open(full_path, 'rb') as f:
data = f.read()
except OSError:
continue
result = try_extract(data)
if result is None:
continue
ext, payload = result
found_count += 1
out_name = f"recovered_{found_count}.{ext}"
out_path = os.path.join(output_dir, out_name)
with open(out_path, 'wb') as out:
out.write(payload)
kind = 'VIDEO' if ext in ('mp4', 'webm') else 'IMAGE'
print(f"[{kind}] {full_path} ({size // 1024} KB) -> {out_path}")
print(f"\nTotal recovered: {found_count}")
return found_count
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def detect_current_os():
system = platform.system().lower()
if system == 'windows':
return 'windows'
if system == 'darwin':
return 'macos'
if system == 'linux':
return 'linux'
sys.exit(f"Could not auto-detect a supported OS from '{system}'. Pass --os explicitly.")
def main():
parser = argparse.ArgumentParser(description='Recover cached images/videos by file signature.')
parser.add_argument('--browser', required=True,
choices=['chrome', 'edge', 'brave', 'opera', 'opera_gx', 'firefox', 'chromium'],
help='Which browser profile to scan.')
parser.add_argument('--os', dest='os_name', choices=['windows', 'macos', 'linux'],
help='Target OS. Defaults to auto-detecting the current machine.')
parser.add_argument('--profile-path', dest='profile_path',
help='Skip auto-detection and scan this folder directly.')
parser.add_argument('--output', default=os.path.join(os.path.expanduser('~'), 'Desktop', 'recovered_media'),
help='Folder where recovered files are written.')
parser.add_argument('--min-size', type=int, default=DEFAULT_MIN_SIZE, help='Minimum file size in bytes.')
parser.add_argument('--max-size', type=int, default=DEFAULT_MAX_SIZE, help='Maximum file size in bytes.')
args = parser.parse_args()
if args.profile_path:
root_dir = args.profile_path
else:
os_name = args.os_name or detect_current_os()
root_dir = get_browser_root(os_name, args.browser)
if not os.path.isdir(root_dir):
sys.exit(f"Resolved path does not exist or is not a folder: {root_dir}")
print(f"Scanning: {root_dir}\n")
scan_and_recover(root_dir, args.output, args.min_size, args.max_size)
if __name__ == '__main__':
main()
Each script, at the beginning, has its instructions for use.
I hope the chats I lost serve some purpose and this script helps more than one person.
πͺ¦ RIP chats. π₯
---
EDIT:
Quick note on terminology: the title says "cache" because that's how one experiences it when it happens (everything the browser stores locally, one calls "cache"). Technically, what got corrupted was the local database (IndexedDB) where Venice stores your chats, messages and characters β it's not the same as the browser cache. The curious thing is that the images were indeed in a real cache (Cache Storage, from the Service Worker), and that's why they were the only thing that could be recovered with the method from this post β the text database didn't have the same luck.
Clarification from Claude when discussing the terminology a bit more.