From immich-photo-manager
Generates an interactive HTML map from geotagged photos, clustered by location with photo counts, date ranges, and album links. Uses Immich MCP server for data extraction.
How this skill is triggered — by the user, by Claude, or both
Slash command
/immich-photo-manager:travel-mapThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Before doing ANYTHING else in this skill, call `ping` on the Immich MCP server.**
Before doing ANYTHING else in this skill, call ping on the Immich MCP server.
ping succeeds → proceed with the skill normally.ping fails or the MCP tools are not available → STOP. Do not continue. Tell the user:❌ Immich is not connected. This plugin needs a running Immich MCP server to work.
Run /setup-immich-photo-manager to configure your Immich connection. You'll need:
- Your Immich server URL (e.g.,
http://192.168.1.100:2283)- An Immich API key (how to create one)
- The MCP server configured (see /setup-immich-photo-manager)
Nothing in this plugin will work until the connection is configured.
Do NOT skip this check. Do NOT try to run any other tool first. Always ping, always block if it fails.
Generate an interactive HTML map showing all locations where photos were taken. Clusters photos by geographic proximity, shows photo counts and date ranges per location, and optionally links to Immich albums.
Get all geotagged photos:
SELECT
"id",
("exifInfo"->>'latitude')::float as lat,
("exifInfo"->>'longitude')::float as lng,
"localDateTime",
"originalPath",
("exifInfo"->>'city') as city,
("exifInfo"->>'state') as state,
("exifInfo"->>'country') as country
FROM asset
WHERE "deletedAt" IS NULL
AND "exifInfo"->>'latitude' IS NOT NULL
AND ("exifInfo"->>'latitude')::float != 0
ORDER BY "localDateTime";
Or use the MCP tool get_map_markers for a lighter dataset.
Group nearby photos into location clusters:
from collections import defaultdict
import math
def haversine(lat1, lng1, lat2, lng2):
"""Distance in km between two GPS points."""
R = 6371
dlat = math.radians(lat2 - lat1)
dlng = math.radians(lng2 - lng1)
a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlng/2)**2
return R * 2 * math.asin(math.sqrt(a))
def cluster_locations(photos, radius_km=15):
"""Simple greedy clustering by distance."""
clusters = []
for photo in photos:
placed = False
for cluster in clusters:
if haversine(photo.lat, photo.lng, cluster.center_lat, cluster.center_lng) < radius_km:
cluster.add(photo)
placed = True
break
if not placed:
clusters.append(Cluster(photo))
return clusters
Alternatively, use the reverse-geocoded city/country from EXIF:
SELECT
"exifInfo"->>'country' as country,
"exifInfo"->>'city' as city,
count(*) as photos,
min("localDateTime") as first_visit,
max("localDateTime") as last_visit,
avg(("exifInfo"->>'latitude')::float) as center_lat,
avg(("exifInfo"->>'longitude')::float) as center_lng
FROM asset
WHERE "deletedAt" IS NULL
AND "exifInfo"->>'latitude' IS NOT NULL
AND "exifInfo"->>'country' IS NOT NULL
GROUP BY country, city
ORDER BY photos DESC;
For each cluster:
Create a standalone HTML file using Leaflet.js:
<!DOCTYPE html>
<html>
<head>
<title>My Travel Map</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/MarkerCluster.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script src="https://unpkg.com/[email protected]/dist/leaflet.markercluster.js"></script>
</head>
<body>
<div id="map" style="height: 100vh; width: 100%"></div>
<script>
const locations = [/* cluster data injected here */];
const map = L.map('map').setView([30, 0], 3);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
const markers = L.markerClusterGroup();
locations.forEach(loc => {
const marker = L.marker([loc.lat, loc.lng])
.bindPopup(`
<strong>${loc.name}</strong><br>
${loc.photos} photos<br>
${loc.first_visit} — ${loc.last_visit}<br>
${loc.visits} visit(s)
`);
markers.addLayer(marker);
});
map.addLayer(markers);
</script>
</body>
</html>
Heatmap layer:
<script src="https://unpkg.com/[email protected]/dist/leaflet-heat.js"></script>
<script>
const heatData = locations.map(l => [l.lat, l.lng, l.photos]);
L.heatLayer(heatData, {radius: 25}).addTo(map);
</script>
Timeline slider: Filter markers by year range using a slider control.
Country statistics panel: Side panel showing countries visited, photos per country, total distance traveled.
Search: Search bar to find a specific location on the map.
| Format | Description |
|---|---|
| Standalone HTML | Self-contained file, opens in any browser, shareable |
| Hosted page | Deploy to your own domain or static hosting |
| Markdown report | Text summary with country list, no map |
| JSON export | Raw cluster data for custom visualization |
npx claudepluginhub drolosoft/immich-photo-manager --plugin immich-photo-managerCreates, curates, and publishes Immich albums organized by geography, theme, or custom criteria. Automates album creation from user prompts like 'create an album from my trip to Italy'.
Automates Google Photos tasks (upload media, manage albums, search photos, batch add items) via Rube MCP (Composio). Always searches tools first for current schemas.
Creates interactive maps, elevation profiles, and spatial visualizations from GPX tracks, waypoints, or route data using R (sf, leaflet, tmap) or Observable (D3, deck.gl). Covers data import, coordinate system handling, map styling, and export to HTML or image formats.