diff --git a/Pipfile b/Pipfile index 095d9f6..6c868ed 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ verify_ssl = true foursquare = "*" ics = {editable = true,git = "https://github.com/C4ptainCrunch/ics.py.git",ref = "bd918ec7453a7cf73a906cdcc78bd88eb4bab71b"} simplekml = "*" +pytz = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index cd195a4..26d7d95 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "67eeb6f5b773d0e59e35ec16559d97c999f7a7b2862284392d7fdf65a280185e" + "sha256": "3a9e4292a546993c8c1f3f01a7183f293efeb295fd8fa776b213273b95279572" }, "pipfile-spec": 6, "requires": { @@ -63,6 +63,14 @@ ], "version": "==2.8.0" }, + "pytz": { + "hashes": [ + "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", + "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" + ], + "index": "pypi", + "version": "==2019.2" + }, "requests": { "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", diff --git a/generate_feeds.py b/generate_feeds.py index 3a8dcc9..71bec56 100755 --- a/generate_feeds.py +++ b/generate_feeds.py @@ -1,11 +1,15 @@ #!/usr/bin/env python3 import argparse import configparser +from datetime import datetime import logging import os +import pytz +from xml.sax.saxutils import escape as xml_escape import foursquare from ics import Calendar, Event +import simplekml logging.basicConfig(level=logging.INFO, format="%(message)s") logger = logging.getLogger(__name__) @@ -59,16 +63,11 @@ class FeedGenerator: logger.info(f"Fetched {checkins} checkin{plural} from the API") if kind == "ics": - data = self._generate_calendar(checkins) - filepath = self.ics_filepath + filepath = self._generate_ics_file(checkins) elif kind == "kml": - data = self._generate_kml(checkins) - filepath = self.kml_filepath + filepath = self._generate_kml_file(checkins) - with open(filepath, "w") as f: - f.writelines(data) - - logger.info(f"Generated {kind} file {filepath}") + logger.info(f"Generated file {filepath}") exit(0) @@ -117,15 +116,31 @@ class FeedGenerator: logger.error(f"Error getting checkins, with offset of {offset}: {err}") exit(1) - def _get_user_url(self): - "Returns the Foursquare URL for the authenticated user." + def _get_user(self): + "Returns details about the authenticated user." try: user = self.client.users() except foursquare.FoursquareException as err: logger.error(f"Error getting user: {err}") exit(1) - return user["user"]["canonicalUrl"] + return user["user"] + + def _generate_ics_file(self, checkins): + """Supplied with a list of checkin data from the API, generates + and saves a .ics file. + + Returns the filepath of the saved file. + + Keyword arguments: + checkins -- A list of dicts, each one data about a single checkin. + """ + calendar = self._generate_calendar(checkins) + + with open(self.ics_filepath, "w") as f: + f.writelines(calendar) + + return self.ics_filepath def _generate_calendar(self, checkins): """Supplied with a list of checkin data from the API, generates @@ -134,7 +149,7 @@ class FeedGenerator: Keyword arguments: checkins -- A list of dicts, each one data about a single checkin. """ - user_url = self._get_user_url() + user = self._get_user() c = Calendar() @@ -143,6 +158,7 @@ class FeedGenerator: # I had some checkins with no data other than # id, createdAt and source. continue + venue_name = checkin["venue"]["name"] tz_offset = self._get_checkin_timezone(checkin) @@ -150,7 +166,7 @@ class FeedGenerator: e.name = f"@ {venue_name}" e.location = venue_name - e.url = f"{user_url}/checkin/{checkin['id']}" + e.url = f"{user['canonicalUrl']}/checkin/{checkin['id']}" e.uid = f"{checkin['id']}@foursquare.com" e.begin = checkin["createdAt"] @@ -175,15 +191,76 @@ class FeedGenerator: return c - def _generate_kml(self, checkins): + def _generate_kml_file(self, checkins): """Supplied with a list of checkin data from the API, generates - a TODO + and saves a kml file. + + Returns the filepath of the saved file. Keyword arguments: checkins -- A list of dicts, each one data about a single checkin. """ + user = self._get_user() - return None + kml = simplekml.Kml() + + # The original Foursquare files had a Folder with name and + # description like this, so: + user_name = f"{user['firstName']} {user['lastName']}" + name = f"foursquare checkin history for {user_name}" + fol = kml.newfolder(name=name, description=name) + + for checkin in checkins: + if "venue" not in checkin: + # I had some checkins with no data other than + # id, createdAt and source. + continue + + venue_name = checkin["venue"]["name"] + tz_offset = self._get_checkin_timezone(checkin) + url = f'https://foursquare.com/v/{checkin["venue"]["id"]}' + + description = [f'@{venue_name}'] + if "shout" in checkin and len(checkin["shout"]) > 0: + description.append('"{}"'.format(checkin["shout"])) + description.append(f"Timezone offset: {tz_offset}") + + coords = [ + ( + checkin["venue"]["location"]["lng"], + checkin["venue"]["location"]["lat"], + ) + ] + + pnt = fol.newpoint( + name=venue_name, + description="".format('\n'.join(description)), + coords=coords, + # Both of these were set like this in Foursquare's original KML: + altitudemode=simplekml.AltitudeMode.relativetoground, + extrude=1, + ) + + # Foursquare's KML feeds had 'updated' and 'published' elements + # in the Placemark, but I don't *think* those are standard, so: + pnt.timestamp.when = ( + datetime.utcfromtimestamp(checkin["createdAt"]) + .replace(tzinfo=pytz.utc) + .isoformat() + ) + + # Use the address, if any: + if "location" in checkin["venue"]: + loc = checkin["venue"]["location"] + if "formattedAddress" in loc and len(loc["formattedAddress"]) > 0: + address = ", ".join(loc["formattedAddress"]) + # While simplexml escapes other strings, it threw a wobbly + # over '&' in addresses, so escape them: + pnt.address = xml_escape(address) + + kml.save(self.kml_filepath) + + return self.kml_filepath def _get_checkin_timezone(self, checkin): """Given a checkin from the API, returns a string representing the