twigots

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 5, 2025 License: MIT Imports: 17 Imported by: 3

README

twigots

Go Reference Go Report Card License - MIT

A go package to fetch ticket listings from the Twickets Live Feed.

Includes utilities to help filter the ticket listings and find the ones you want!

Powers (the similarly creatively named) twitchets, a tool to watch for event ticket listings on Twickets and notify you so you can snap them up! 🫰

Installation

go get -u github.com/ahobsonsayers/twigots

Getting an API Key

To use this tool, you will need a Twickets API key. Although Twickets doesn't provide a free API, you can easily obtain a key by following these steps:

  1. Visit the Twickets Live Feed
  2. Open your browser's Developer Tools (F12) and navigate to the Network tab
  3. Look for the GET request to https://www.twickets.live/services/catalogue and copy the api_key query parameter. You might need to refresh the page first if nothing appears in this tab.

This API key is not provided here due to liability concerns, but it appears to be a fixed, unchanging value.

Example Usage

[!Warning] Although this package is functional and ready for use, it is still a work in progress and is subject to change without notice - the API and usage may be modified at any time.

Use with caution and check for updates regularly.

Example can be seen in example/main.go or below:

package main

import (
	"context"
	"log"
	"log/slog"
	"time"

	"github.com/ahobsonsayers/twigots"
	"github.com/ahobsonsayers/twigots/filter"
)

func main() {
	apiKey := "my_api_key"

	// Create twickets client (using api key)
	client, err := twigots.NewClient(apiKey)
	if err != nil {
		log.Fatal(err)
	}

	// Fetch ticket listings
	listings, err := client.FetchTicketListings(
		context.Background(),
		twigots.FetchTicketListingsInput{
			// Required
			Country: twigots.CountryUnitedKingdom, // Only UK is supported at the moment
			// Optional. See all options in godoc
			CreatedBefore: time.Now(),
			CreatedAfter:  time.Now().Add(time.Duration(-5 * time.Minute)), // 5 mins ago
			MaxNumber:     100,
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("Fetched %d ticket listings", len(listings))

	// Filter ticket listings just by name
	// Use the default event name similarity (0.9) to allow minor mismatches
	hamiltonListings := filter.FilterTicketListings(
		listings,
		filter.EventName("Hamilton", filter.DefaultEventNameSimilarity),
	)
	for _, listing := range hamiltonListings {
		slog.Info(
			"Found Hamilton ticket listing",
			"Event", listing.Event.Name,
			"NumTickets", listing.NumTickets,
			"Price", listing.TotalPriceInclFee().String(),
			"OriginalPrice", listing.OriginalTicketPrice().String(),
			"URL", listing.URL(),
		)
	}

	// Filter ticket listings just by several filters
	coldplayListings := filter.FilterTicketListings(
		listings,
		filter.EventName("Coldplay", 1), // Event name similarity of 1 - exact match only
		filter.EventRegion( // Only in specific regions
			twigots.RegionLondon,
			twigots.RegionSouth,
		),
		filter.NumTickets(2),    // Exactly 2 tickets in the listing
		filter.MinDiscount(0.1), // Discount of > 10%
	)
	for _, listing := range coldplayListings {
		slog.Info(
			"Found Coldplay ticket listing",
			"Event", listing.Event.Name,
			"NumTickets", listing.NumTickets,
			"Price", listing.TotalPriceInclFee().String(),
			"OriginalPrice", listing.OriginalTicketPrice().String(),
			"URL", listing.URL(),
		)
	}
}

How does the event name matching/similarity work?

Event name similarity is calculated using a modified Smith-Waterman-Gotoh algorithm. The complexity behind this algorithm does not need to be understood, but for all intents and purposes, it can be thought of as fuzzy substring matching.

If the desired event name appears within the actual event name returned by twickets (as a substring), the event similarity will be 1. Equally, if the desired event name does not appear at all, the similarity will be 0.

Setting a required similarity below, but close to 1, will allow for small inconsistencies due to misspelling etc., but can return false positives. We recommend (and default to) a value of 0.9.

False positives can also occur if your desired event name appears in the actual event name, but the event is not the one you want. This can often happen with things like tribute bands - see the example below.

Example:

Desired event: Taylor Swift
Actual event: Taylor Swift: The Eras Tour
Similarity score: 1

Example of a false positive:

Desired event: Taylor Swift
Actual event: Miss Americana: A Tribute to Taylor Swift
Similarity score: 1 <- This is a exact match, but it is probably not the event we want

For a more in depth explanation of the string matching algorithm, see this PR.

Normalization

To help with matching, both the desired and actual event names are normalized before similarity is calculated.

This is done by:

  • Converting to lower case
  • Removing all symbols/non-alphanumeric characters (except & - see below)
  • Replacing all & symbols with and
  • Removing any the prefix
  • Trimming leading and trailing whitespace and replacing all 2+ whitespace with a single space
  • Replacing accented characters with their non-accented characters
  • Spaces are added to either side of the string, to help avoid cases where the word appears inside another word e.g. grate shouldn't match ungrateful

Why the name twigots?

Because its a stupid mash up of Tickets and Go... and also why not?

Hits

Documentation

Index

Constants

View Source
const (
	TwicketsURL = "https://www.twickets.live"
)

Variables

View Source
var (
	CountryUnitedKingdom = country.Add(Country{"GB"})

	Countries = country.Enum()
)
View Source
var (
	RegionEastAnglia     = region.Add(Region{"GBEA"})
	RegionLondon         = region.Add(Region{"GBLO"})
	RegionMidlands       = region.Add(Region{"GBMI"})
	RegionNorth          = region.Add(Region{"GBNO"})
	RegionNorthEast      = region.Add(Region{"GBNE"})
	RegionNorthernIsland = region.Add(Region{"GBNI"})
	RegionNorthWest      = region.Add(Region{"GBNW"})
	RegionScotland       = region.Add(Region{"GBSC"})
	RegionSouth          = region.Add(Region{"GBSO"})
	RegionSouthEast      = region.Add(Region{"GBSE"})
	RegionSouthWest      = region.Add(Region{"GBSW"})
	RegionWales          = region.Add(Region{"GBWA"})

	Regions = region.Enum()
)
View Source
var (
	CurrencyGBP = currency.Add(Currency{"GBP"})

	Currencies = currency.Enum()
)

Functions

func FeedUrl

func FeedUrl(input FeedUrlInput) (string, error)

FeedUrl gets the url of a ticket listings feed. Note: The number of ticket listings (that are non-delisted) in the feed at this url will ALWAYS be 10. There may be any number of additional delisted ticket listings.

Format is: https://www.twickets.live/services/catalogue?q=countryCode=GB&count=10&api_key=<api_key>

func ListingURL

func ListingURL(listingId string, numTickets int) string

ListingURL gets the url of a listing given its id and the number of tickets in the listing.

Format is: https://www.twickets.live/app/block/<ticketId>,<numTickets>

func ValidateURL added in v0.7.0

func ValidateURL(urlString string) error

ValidateURL checks if a url string is a valid.

Types

type Artist

type Artist struct {
	Id   string `json:"id"`
	Name string `json:"name"`
	Slug string `json:"linkName"`
}

Artist contains the details of an artist.

type Client

type Client struct {
	// contains filtered or unexported fields
}

func NewClient

func NewClient(apiKey string, opts ...ClientOpt) (*Client, error)

NewClient creates a new Twickets client

func (*Client) Client added in v0.2.0

func (c *Client) Client() *http.Client

func (*Client) FetchTicketListings

func (c *Client) FetchTicketListings(
	ctx context.Context,
	input FetchTicketListingsInput,
) (TicketListings, error)

FetchTicketListings gets ticket listings using the specified input.

func (*Client) FetchTicketListingsByFeedUrl

func (c *Client) FetchTicketListingsByFeedUrl(
	ctx context.Context,
	feedUrl string,
) (TicketListings, error)

FetchTicketListings gets ticket listings using the specified feel url.

type ClientOpt added in v0.7.0

type ClientOpt func(*req.Client) error

func WithFlareSolverr added in v0.7.0

func WithFlareSolverr(flareSolverrUrl string) ClientOpt

type Country

type Country enum.Member[string]

func (Country) MarshalJSON added in v0.6.1

func (c Country) MarshalJSON() ([]byte, error)

func (*Country) UnmarshalJSON

func (c *Country) UnmarshalJSON(data []byte) error

func (*Country) UnmarshalText

func (c *Country) UnmarshalText(data []byte) error

type Currency

type Currency enum.Member[string]

func (Currency) MarshalJSON added in v0.6.1

func (c Currency) MarshalJSON() ([]byte, error)

func (Currency) Symbol

func (c Currency) Symbol() string

Symbol is the character that represents the currency e.g. $, £, €.

func (*Currency) UnmarshalJSON

func (c *Currency) UnmarshalJSON(data []byte) error

type Date

type Date struct{ time.Time }

Date is a date (with no time).

func (*Date) UnmarshalJSON

func (d *Date) UnmarshalJSON(data []byte) error

type DateTime

type DateTime struct{ time.Time }

DateTime is a date and time.

func (*DateTime) UnmarshalJSON

func (dt *DateTime) UnmarshalJSON(data []byte) error

type Event

type Event struct {
	Id       string `json:"id"`
	Name     string `json:"eventName"`
	Category string `json:"category"`

	Date      Date      `json:"date"`
	Time      Time      `json:"showStartingTime"`
	OnSale    *DateTime `json:"onSaleTime"` // 2023-11-17T10:00:00Z
	Announced *DateTime `json:"created"`    // 2023-11-17T10:00:00Z

	Venue  Venue    `json:"venue"`
	Lineup []Lineup `json:"participants"`
}

Event contains the details of an event.

type FeedUrlInput

type FeedUrlInput struct {
	// Required fields
	APIKey  string
	Country Country

	// Optional fields
	Regions    []Region  // Defaults to all country regions
	BeforeTime time.Time // Defaults to current time
}

func (FeedUrlInput) Validate added in v0.1.3

func (f FeedUrlInput) Validate() error

Validate the input struct used to get the feed url. This is used internally to check the input, but can also be used externally.

type FetchTicketListingsInput

type FetchTicketListingsInput struct {
	// Required fields
	Country Country

	// Regions for which to fetch ticket listings from.
	// Leave this unset or empty to fetch listings from any region.
	// Defaults to any region (unset).
	Regions []Region

	// MaxNumber is the maximum number of ticket listings to fetch.
	// If getting ticket listings within in a time period using `CreatedAfter`, set this to an arbitrarily
	// large number (e.g. 250) to ensure all listings in the period are fetched, while preventing
	// accidentally fetching too many listings and possibly being rate limited or blocked.
	// Defaults to 10.
	// Set to -1 if no limit is desired. This is dangerous and should only be used with well constrained time periods.
	MaxNumber int

	// CreatedAfter is the time which ticket listings must have been created after to be fetched.
	// Set this to fetch listings within a time period.
	// Set `MaxNumber` to an arbitrarily large number (e.g. 250) to ensure all listings in the period are fetched,
	// while preventing  accidentally fetching too many listings and possibly being rate limited or blocked.
	CreatedAfter time.Time

	// CreatedBefore is the time which ticket listings must have been created before to be fetched.
	// Set this to fetch listings within a time period.
	// Defaults to current time.
	CreatedBefore time.Time
}

FetchTicketListingsInput defines parameters when getting ticket listings.

Ticket listings can either be fetched by maximum number or by time period. The default is to get a maximum number of ticket listings.

If both a maximum number and a time period are set, whichever condition is met first will stop the fetching of ticket listings.

func (FetchTicketListingsInput) Validate added in v0.1.3

func (f FetchTicketListingsInput) Validate() error

Validate the input struct used to get ticket listings. This is used internally to check the input, but can also be used externally.

type Lineup

type Lineup struct {
	Artist  Artist `json:"participant"`
	Billing int    `json:"billing"`
}

Lineup contains the details of the event lineup.

type Location

type Location struct {
	Id       string  `json:"id"`
	Name     string  `json:"shortName"`
	FullName string  `json:"name"`
	Country  Country `json:"countryCode"`
	Region   Region  `json:"regionCode"`
}

Venue contains the details of an event location.

type Price

type Price struct {
	Currency Currency `json:"currencyCode"`

	// Amount is the cost in Cents, Pennies etc.
	// Prefer using `Number`
	Amount int `json:"amountInCents"`
}

func (Price) Add

func (p Price) Add(other Price) Price

Add prices together. Currency will be kept. Returns a new price.

func (Price) Divide

func (p Price) Divide(num int) Price

Divide prices. Currency will be kept. Returns a new price.

func (Price) Multiply

func (p Price) Multiply(num int) Price

Multiply prices together. Returns a new price.

func (Price) Number

func (p Price) Number() float64

Number is the numerical value of the price. E.g. Dollars, Pounds, Euros etc. Use this over `Amount“.

func (Price) String

func (p Price) String() string

The price as a string. e.g. $30.62

func (Price) Subtract

func (p Price) Subtract(other Price) Price

Subtract price from another. Currency will be kept. Returns a new price.

type Region

type Region enum.Member[string]

func (Region) MarshalJSON added in v0.6.1

func (r Region) MarshalJSON() ([]byte, error)

func (*Region) UnmarshalJSON

func (r *Region) UnmarshalJSON(data []byte) error

func (*Region) UnmarshalText

func (r *Region) UnmarshalText(data []byte) error

type TicketListing

type TicketListing struct {
	Id        string   `json:"blockId"`
	CreatedAt UnixTime `json:"created"`
	ExpiresAt UnixTime `json:"expires"`

	// Number of tickets in the listing
	NumTickets int `json:"ticketQuantity"`

	// TotalPriceExclFee is the total price of all tickets, excluding fee.
	// Use TotalPriceInclFee to get the total price of all tickets, including fee.
	// Use TicketPriceExclFee to get the price of a single ticket, excluding fee.
	// Use TicketPriceInclFee to get the price of a single ticket, including fee.
	TotalPriceExclFee Price `json:"totalSellingPrice"`

	// TwicketsFee is the total twickets fee for all tickets.
	// Use TwicketsFeePerTicket to get the twickets fee per ticket.
	TwicketsFee Price `json:"totalTwicketsFee"`

	// OriginalTotalPrice is the original total price of all tickets, including any fee.
	// Use OriginalTicketPrice to get the original price of a single ticket, including any fee.
	OriginalTotalPrice Price `json:"faceValuePrice"`

	SellerWillConsiderOffers bool `json:"sellerWillConsiderOffers"`

	// The type of the ticket e.g. seated, Standing, Box etc.
	TicketType   string `json:"priceTier"`
	SeatAssigned bool   `json:"seatAssigned"`
	Section      string `json:"section"` // Can be empty
	Row          string `json:"row"`     // Can be empty

	Event Event `json:"event"`
	Tour  Tour  `json:"tour"`
}

TicketListing is a listing of ticket(s) on Twickets

func UnmarshalTwicketsFeedJson

func UnmarshalTwicketsFeedJson(data []byte) ([]TicketListing, error)

func (TicketListing) Discount

func (l TicketListing) Discount() float64

Discount is the discount on the original price of a single ticket, including any fee.

Discount is returned as a value between 0 and 1 (with 1 representing 100% off). If ticket is being sold at its original price, the addition of the twickets fee will cause discount to be < 0 i.e. the total ticket price will have gone up.

func (TicketListing) DiscountString

func (l TicketListing) DiscountString() string

DiscountString is the discount on the original price of a single ticket, including any fee as a percentage string between 0-100 with a % suffix.

If ticket is being sold at its original price, the addition of the twickets fee will cause discount to be < 0% i.e. the total ticket price will have gone up. If this is the / case "none" will be returned instead of a negative percentage

func (TicketListing) OriginalTicketPrice

func (l TicketListing) OriginalTicketPrice() Price

OriginalTotalPrice is the original price of a single ticket, including any fee.

Use OriginalTotalPrice to get the original total price of all tickets, including any fee.

func (TicketListing) TicketPriceExclFee

func (l TicketListing) TicketPriceExclFee() Price

TicketPriceExclFee is price of a single ticket, excluding fee.

Use TotalPriceExclFee to get the total price of all tickets, excluding fee.

Use TotalPriceInclFee to get the total price of all tickets, including fee.

Use TicketPriceInclFee to get the price of a single ticket, including fee.

func (TicketListing) TicketPriceInclFee

func (l TicketListing) TicketPriceInclFee() Price

TicketPriceExclFee is price of a single ticket, including fee.

Use TotalPriceExclFee to get the total price of all tickets, excluding fee.

Use TotalPriceInclFee to get the total price of all tickets, including fee.

Use TicketPriceExclFee to get the price of a single ticket, excluding fee.

func (TicketListing) TotalPriceInclFee

func (l TicketListing) TotalPriceInclFee() Price

TotalPriceExclFee is the total price of all tickets, including fee.

Use TotalPriceExclFee to get the total price of all tickets, excluding fee.

Use TicketPriceExclFee to get the price of a single ticket, excluding fee.

Use TicketPriceInclFee to get the price of a single ticket, including fee.

func (TicketListing) TwicketsFeePerTicket

func (l TicketListing) TwicketsFeePerTicket() Price

TwicketsFeePerTicket is the twickets fee per ticket.

Use TwicketsFee to get the total fee for all tickets.

func (TicketListing) URL

func (l TicketListing) URL() string

URL of the ticket listing

Format is: https://www.twickets.live/app/block/<ticketId>,<quanitity>

type TicketListings

type TicketListings []TicketListing

TicketListings is a slice of ticket listings.

func (TicketListings) GetById

func (l TicketListings) GetById(id string) *TicketListing

GetById gets the first ticket listing with a matching id, or returns nil if one does not exist.

type Time

type Time struct{ time.Time }

Date is a time (with no date).

func (*Time) UnmarshalJSON

func (t *Time) UnmarshalJSON(data []byte) error

type Tour

type Tour struct {
	Id         string   `json:"tourId"`
	Name       string   `json:"tourName"`
	Slug       string   `json:"slug"`
	FirstEvent *Date    `json:"minDate"`      // 2024-06-06
	LastEvent  *Date    `json:"maxDate"`      // 2024-11-14
	Countries  []string `json:"countryCodes"` // TODO use enum - requires all countries to be added
}

Event contains the details of a tour.

type UnixTime

type UnixTime struct{ time.Time }

UnixTime is a time from unix time.

func (*UnixTime) UnmarshalJSON

func (t *UnixTime) UnmarshalJSON(data []byte) error

type Venue

type Venue struct {
	Id       string   `json:"id"`
	Name     string   `json:"name"`
	Location Location `json:"location"`
	Postcode string   `json:"postcode"`
}

Venue contains the details of an event venue.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL