Finding Concerts with Powershell - Part 2
I had a few minutes to get back to that PowerShell code that found upcoming concerts in St. Louis, and I added a bit. Skip ahead for the code if you most - the most notable feature upgrade includes finding the closest place that the target band is playing and displaying that show's City/Region and drive time.
Geolocation / Google Distance API
Finding bands who were playing shows in a given city was pretty easy, but what if a band I want to see is playing in a suburb of my target city? Or what about if the band is playing one town over and I would totally be willing to drive the 30 minutes to see the show?
I already have a full list of all the events/venues where each band is playing, I just need to parse them to determine the closest place they're playing if they're not playing in my target City. I originally thought about comparing the target city's latitude and longitude to each event venue's latitude and longitude. Unfortunately, there's some serious math that goes into figuring out the distance between two points (See this Stackoverflow discussion to get an idea how crazy things can get). I started down the path of doing a simplified version of comparison to try and limit the calls to an API (Google Maps, Openmaps, Mapquest, etc), but when that failed, I just oped to use Google's Distance Matrix API as it allows 2.5k requests per day.
Integrating with Google's API was rather simple. I requested a new API key, then followed their documentation to craft an Invoke-RESTMethod
call.
$key = "<My API Key>"
$origin = "St. Louis, MO"
$destination = "Chicago, IL"
$result = Invoke-RestMethod "https://maps.googleapis.com/maps/api/distancematrix/json?origins=$origin&destinations=$destination&sensor=false&key=$key"
The resulting JSON includes various helpful bits of information like distance in KM: $result.rows[0].elements[0].distance.value
and driving duration: $result.rows[0].elements[0].duration.text
. Once I had each venue's distance in KM, a simple comparison enabled me to find the shortest distance, and record the appropriate information (Venue, City/Region, Driving Duration, etc).
Final Tweaks
I added proper help syntax then saved the cmdlet as get-UpcomingConcerts.ps1. Other additions included a progress bar that details which band is currently being examined and a sorted output: first bands that are performing in the target city, then bands that are outside of the city. Various comments, verbose/debug statements, and other formatting fixes went into the finished product found below.
If you wish to use this code yourself, sign up for an API key from Google and enable the Google Distance Matrix API, replacing the value of $key with your API key. You may adjust your target band list in code, or provide it as an array to the -targetBands parameter.
Get-UpcomingShows.ps1
<#
.SYNOPSIS
Utilizes the BandsInTown API to search for a given band(s) in a given city, or
surrounding cities
.DESCRIPTION
Iterates through all TargetBands and uses the BandsInTown API to determine if
the specific band is playing in the given city. If they are, helpful
information is provided.
.EXAMPLE
.\get-UpcomingConcerts.ps1 -TargetBands "NOFX", "The Blaggards" -TargetCity "Boston"
This will output the event details if NOFX or The Blaggards are playing in Boston, MA.
.EXAMPLE
.\get-UpcomingConcerts.ps1 -TargetBands "NOFX", "The Blaggards"
This will output the event details if NOFX or The Blaggards are playing in the default
city, St. Louis, MO.
.PARAMETER TargetBands
An array of strings, this variable contains the target bands to search for.
.PARAMETER TargetCity
This is the target city for which concerts will be filtered.
.NOTES
Author: Brian Marsh
Version: 1.0
#>
[CmdletBinding()]
param( [String[]]$TargetBands,
$TargetCity = "St Louis")
BEGIN {
# If no target band(s) were provided, let's do some defaults!
if (!($TargetBands)){
$TargetBands = "Mighty Mighty Bosstones", "Taking Back Sunday", "Chris Thile"
$TargetBands += "NOFX", "Reel Big Fish", "Surburban Legends", "H2O", "Sick of it all"
$TargetBands += "New Found Glory", "Ingrid Michaelson", "Dropkick Murphys", "Mad Caddies"
$TargetBands += "Coheed and Cambria", "Placebo", "Foo Fighters", "Rush", "Bastille"
$TargetBands += "Chuck Ragan", "Rancid", "Bad Religon", "Bouncing Souls"
}
$targetCityShows = @()
$remoteShows = @()
# Quick function to use Google APIs to determine distance, and compare all given events
# for the geographically closest event
function get-ClosestVenue{
param ( $events )
# Google API Key
$key = "<Get your key and put it here>"
# Set Default value for min distance
$minDistance = 9999999
# Create a custom PSObject to hold pertinent information
$goodEvent = New-Object -TypeName PSObject
$goodEvent | Add-Member -MemberType NoteProperty -Name "City" -value ""
$goodEvent | Add-Member -MemberType NoteProperty -Name "Region" -value ""
$goodEvent | Add-Member -MemberType NoteProperty -Name "Duration" -value ""
$goodEvent | Add-Member -MemberType NoteProperty -Name "Datetime" -value ""
# Iterate through each event
foreach ($event in $events) {
# Ignore venues outside the US for now (omit this line for international searches)
if ($event.venue.country -match "United States" -or $event.venue.country -match "Canada") {
# Set the destination to this Event's City and Region/state
$destination = $($event.venue.city+", "+$event.venue.region)
# We're starting in Target City
$origin = $TargetCity
# Get the destination JSON detailing from Origin to Destination
$result = Invoke-RestMethod "https://maps.googleapis.com/maps/api/distancematrix/json?origins=$origin&destinations=$destination&sensor=false&key=$key"
# Select only the kilometer's value
$km = $result.rows[0].elements[0].distance.value
# If our Minimum distance is greater than this distance, we have a shorter route!
if ($minDistance -gt $km) {
# Set our new Minimum Distance
$minDistance = $result.rows[0].elements[0].distance.value
# Grab the appropriate information and save it to the custom PSObject
$goodEvent.City = $event.venue.city
$goodEvent.Region = $event.venue.region
$goodEvent.Duration = $result.rows[0].elements[0].duration.text
$goodEvent.Datetime = $event.datetime
}
}
}
# Output the Custom PSObject data
$goodEvent | select City, Region, Duration, Datetime
}
}
PROCESS {
# Set a band counter
$count = 0
# Iterate through all the target bands
foreach ($TargetBand in $TargetBands) {
# Show some progress bar for this activity
write-progress -activity "Search in Progress" -status "Band: $TargetBand" -percentcomplete ($count/$TargetBands.Length * 100)
# Initialize variables
$r = $events = $null
$playingSoon = $false
# Use Try/Catch to deal with exceptions here
try {
# Get the appropriate body for a RestMethod
$r = Invoke-WebRequest "http://api.bandsintown.com/artists/$TargetBand/events.json?api_version=2.0&app_id=PowerShell" -ErrorAction Stop -Verbose:$false
} catch {
Write-Debug "Something went wrong... debug?"
}
# Retrieve all events that this band is performing in from BandsInTown.com
$events = Invoke-RestMethod http://api.bandsintown.com/artists/$TargetBand/events.json -Body $r -Verbose:$false
# Iterate through each event
foreach ($event in $events) {
# Grab this Event's Venue/City
$myVenue = $event.venue
$city = $myVenue.city
# If this is happening in our target city
if ($TargetCity -match $city){
# Start output message accordingly
$msg = "$TargetBand is playing in $TargetCity at $($event.venue.name) on $([Datetime]$event.datetime)."
# Check ticket status and append appropriate information to the message
if ($event.ticket_status -match "available") {
$msg += " Tickets are available now at: $($event.ticket_url)"
} else {
$msg += " Tickets are unavailable."
}
# Output the message
$targetCityShows += $msg
# Set flag variable to true
$playingSoon = $true
}
}
# If the target band is not playing soon
if ($playingSoon -eq $false){
# Check if there are any events at all
if ($events) {
# Extra Verbose output
Write-Verbose "$TargetBand is not playing in $TargetCity for the forseeable future."
# Get the closest venue in the array of events
$closest = get-ClosestVenue $events
# Output the closest venue
$msg = "$TargetBand is playing is $($closest.duration) drive away in $($closest.city+", "+$closest.region) on $([DateTime]$closest.datetime)"
Write-Debug "Stahp?"
$remoteShows += $msg
} else {
# Extra verbose output
Write-Verbose "Sorry, $TargetBand is not playing a show in the forseeable future."
}
}
# Increase band counter
$count ++
}
}
END {
# Last minute debugging
Write-Debug "Anything Else?"
Write-Output "Shows in $targetCity"
Write-Output $targetCityShows
Write-Output ""
Write-Output "Closest Shows"
Write-Output $remoteShows
}