#!/usr/bin/env python3 import json import sys from datetime import datetime, timezone from xml.sax.saxutils import escape OSMAND_NS = "https://osmand.net" def epoch_to_iso_z(value): if value is None: return None try: ts = int(value) return datetime.fromtimestamp(ts, tz=timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") except Exception: return None def norm(s): if s is None: return None s = str(s).strip() return s if s else None def main(): if len(sys.argv) not in (3, 4): print( "Usage: nextcloud_favorites_to_osmand_prefixed_groups.py .favorites.json out.gpx [prefix]\n" "Example: nextcloud_favorites_to_osmand_prefixed_groups.py .favorites.json favourites.gpx Norwegen", file=sys.stderr ) sys.exit(2) in_path = sys.argv[1] out_path = sys.argv[2] prefix = sys.argv[3] if len(sys.argv) == 4 else "Norwegen" with open(in_path, "r", encoding="utf-8") as f: data = json.load(f) if not isinstance(data, dict) or data.get("type") != "FeatureCollection": raise SystemExit("Input is not a GeoJSON FeatureCollection") features = data.get("features", []) if not isinstance(features, list): raise SystemExit("GeoJSON 'features' is not a list") wpts = [] for idx, feat in enumerate(features): if not isinstance(feat, dict) or feat.get("type") != "Feature": continue geom = feat.get("geometry") or {} if geom.get("type") != "Point": continue coords = geom.get("coordinates") if not (isinstance(coords, list) and len(coords) >= 2): continue # GeoJSON order: [lon, lat] try: lon = float(coords[0]) lat = float(coords[1]) except Exception: continue props = feat.get("properties") or {} title = norm(props.get("Title")) or f"Favorite {idx+1}" comment = norm(props.get("Comment")) category = norm(props.get("Category")) or "Uncategorized" group = f"{prefix}/{category}" # Prefer Updated, else Published (epoch seconds) time_iso = epoch_to_iso_z(props.get("Updated", props.get("Published"))) wpts.append({ "lat": lat, "lon": lon, "name": title, "desc": comment, "time": time_iso, "group": group, "category": category }) if not wpts: raise SystemExit("No Point features found to convert") with open(out_path, "w", encoding="utf-8") as f: f.write('\n') f.write('\n') f.write(' \n') f.write(f' \n') f.write(' \n') for w in wpts: f.write(f' \n') f.write(f' {escape(w["name"])}\n') # OsmAnd favorites group name (most important) f.write(f' {escape(w["group"])}\n') if w["desc"] is not None: f.write(f' {escape(w["desc"])}\n') if w["time"] is not None: f.write(f' \n') # Extra hints (safe if ignored) f.write(' \n') f.write(f' {escape(w["group"])}\n') f.write(f' \n') f.write(' \n') f.write(' \n') f.write('\n') print(f"Wrote {len(wpts)} favorites to {out_path} with groups like '{prefix}/'") if __name__ == "__main__": main()