wtw_uk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/COMM-LICENSE +22 -0
- data/LICENSE +617 -0
- data/README.md +160 -0
- data/lib/wtw_uk/cinema.rb +130 -0
- data/lib/wtw_uk/internal/http.rb +41 -0
- data/lib/wtw_uk/internal/parser/programme.rb +46 -0
- data/lib/wtw_uk/internal/parser/showtime.rb +89 -0
- data/lib/wtw_uk/internal/title_sanitizer.rb +38 -0
- data/lib/wtw_uk/internal/website.rb +29 -0
- data/lib/wtw_uk/performance.rb +67 -0
- data/lib/wtw_uk/version.rb +6 -0
- data/lib/wtw_uk.rb +19 -0
- metadata +152 -0
data/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# WtwUk
|
|
2
|
+
|
|
3
|
+
A simple gem to pull cinemas and performances from the [WTW Cinemas website](https://wtwcinemas.co.uk) and spit out useful formatted info.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
gem 'wtw_uk'
|
|
10
|
+
|
|
11
|
+
And then execute:
|
|
12
|
+
|
|
13
|
+
$ bundle
|
|
14
|
+
|
|
15
|
+
Or install it yourself as:
|
|
16
|
+
|
|
17
|
+
$ gem install wtw_uk
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
The gem conforms to the API set down in the `cinebase` gem, [andycroll/cinebase](https://github.com/andycroll/cinebase), which provides a lot of useful base vocabulary and repetitive code for this series of cinema focussed gems.
|
|
22
|
+
|
|
23
|
+
Performance titles are sanitized as much as possible, removing years,
|
|
24
|
+
subtitle, preview, autism friendly, Q&A and film club markers, and naming
|
|
25
|
+
National Theatre Live, Royal Ballet and Opera and Exhibition on Screen
|
|
26
|
+
screenings in full.
|
|
27
|
+
|
|
28
|
+
No token or setup is needed. WTW's WordPress website runs Jacro's cinema
|
|
29
|
+
plugin, with a subsite for each venue. Tickets are sold through TapOS,
|
|
30
|
+
behind each venue's own booking pages.
|
|
31
|
+
|
|
32
|
+
### Cinema
|
|
33
|
+
|
|
34
|
+
The gem holds WTW's four Cornish venues, keyed by the TapOS site code in
|
|
35
|
+
their booking pages: subsites and Jacro cinema ids from the home page,
|
|
36
|
+
addresses from each venue's page footer and coordinates from OpenStreetMap.
|
|
37
|
+
Cinema methods make no requests.
|
|
38
|
+
|
|
39
|
+
``` ruby
|
|
40
|
+
WtwUk::Cinema.all
|
|
41
|
+
#=> [<WtwUk::Cinema ...>, <WtwUk::Cinema ...>, ...]
|
|
42
|
+
|
|
43
|
+
cinema = WtwUk::Cinema.new('PLATRU')
|
|
44
|
+
#=> <WtwUk::Cinema ...>
|
|
45
|
+
|
|
46
|
+
cinema.adr
|
|
47
|
+
#=> {
|
|
48
|
+
street_address: '69 Lemon Street',
|
|
49
|
+
extended_address: nil,
|
|
50
|
+
locality: 'Truro',
|
|
51
|
+
region: 'Cornwall',
|
|
52
|
+
postal_code: 'TR1 2PN',
|
|
53
|
+
country_name: 'United Kingdom'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
cinema.brand
|
|
57
|
+
#=> 'WTW Cinemas'
|
|
58
|
+
|
|
59
|
+
cinema.full_name
|
|
60
|
+
#=> 'WTW Cinemas Truro'
|
|
61
|
+
|
|
62
|
+
cinema.id
|
|
63
|
+
#=> 'PLATRU'
|
|
64
|
+
|
|
65
|
+
cinema.latitude
|
|
66
|
+
#=> 50.261851
|
|
67
|
+
|
|
68
|
+
cinema.url
|
|
69
|
+
#=> 'https://wtwcinemas.co.uk/truro/'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| id | name |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `WTWNEW` | Newquay (Lighthouse Cinema) |
|
|
75
|
+
| `FILAUS` | St Austell (White River Cinema) |
|
|
76
|
+
| `PLATRU` | Truro (The Plaza) |
|
|
77
|
+
| `REGWAD` | Wadebridge (The Regal) |
|
|
78
|
+
|
|
79
|
+
### Performances
|
|
80
|
+
|
|
81
|
+
`Performance.at` returns performances for an inclusive range of showing dates,
|
|
82
|
+
defaulting to today in London and the following 13 days. The what's on page's
|
|
83
|
+
'All Shows' request lists every showtime the venue has on sale, so any range
|
|
84
|
+
is one request.
|
|
85
|
+
|
|
86
|
+
``` ruby
|
|
87
|
+
WtwUk::Performance.at('PLATRU', from: Date.today, to: Date.today + 2)
|
|
88
|
+
#=> [<WtwUk::Performance ...>, <WtwUk::Performance ...>, ...]
|
|
89
|
+
|
|
90
|
+
performance = WtwUk::Performance.at('PLATRU').first
|
|
91
|
+
#=> <WtwUk::Performance ...>
|
|
92
|
+
|
|
93
|
+
performance.film_name
|
|
94
|
+
#=> 'Heart of the Beast' # from 'Heart of the Beast (with subtitles)'
|
|
95
|
+
|
|
96
|
+
performance.film_year
|
|
97
|
+
#=> nil # from the title, e.g. 1971 for 'The Devils (1971)'
|
|
98
|
+
|
|
99
|
+
performance.dimension
|
|
100
|
+
#=> '2d'
|
|
101
|
+
|
|
102
|
+
performance.variant
|
|
103
|
+
#=> ['senior', 'subtitled']
|
|
104
|
+
|
|
105
|
+
performance.subtitled?
|
|
106
|
+
#=> true
|
|
107
|
+
|
|
108
|
+
performance.festival
|
|
109
|
+
#=> nil
|
|
110
|
+
|
|
111
|
+
performance.starting_at
|
|
112
|
+
#=> 2026-11-12 11:15:00 UTC
|
|
113
|
+
|
|
114
|
+
performance.booking_url
|
|
115
|
+
#=> 'https://wtwcinemas.co.uk/truro/booknow/122882/'
|
|
116
|
+
|
|
117
|
+
performance.cinema_name
|
|
118
|
+
#=> 'Truro'
|
|
119
|
+
|
|
120
|
+
performance.cinema_id
|
|
121
|
+
#=> 'PLATRU'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Booking links go to the venue's booking page, which holds the TapOS seat
|
|
125
|
+
selection. Showtimes already passed today have no booking link.
|
|
126
|
+
|
|
127
|
+
Variants come from each showtime's classes and the title: `arts` (event
|
|
128
|
+
cinema: theatre, opera, ballet, musicals, concerts and exhibitions), `q&a`,
|
|
129
|
+
`relaxed` (autism friendly), `senior` (Silver Screen) and `subtitled`. Many
|
|
130
|
+
screenings offer audio description headsets or caption glasses, so neither
|
|
131
|
+
is a variant. Laser projection, strobe warnings and ticket offers are not
|
|
132
|
+
variants either.
|
|
133
|
+
|
|
134
|
+
## Tests
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
bundle exec rake # unit tests against fixtures
|
|
138
|
+
bundle exec rake live # live wtwcinemas.co.uk
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The fixture in `test/fixtures` is Truro's programme, trimmed to a few films
|
|
142
|
+
with shortened descriptions. `test/support/api_shapes.rb` describes the
|
|
143
|
+
markup the gem reads. The unit suite checks the fixture against it and the
|
|
144
|
+
live suite checks every venue's real programme, held only in memory, along
|
|
145
|
+
with the venue list and each venue's TapOS code. When the markup changes,
|
|
146
|
+
update the shape first, then the fixture.
|
|
147
|
+
|
|
148
|
+
## Releasing
|
|
149
|
+
|
|
150
|
+
Bump the version, merge, then run the **Release Gem** workflow in GitHub
|
|
151
|
+
Actions. It runs the tests, tags the version and publishes the gem through
|
|
152
|
+
RubyGems trusted publishing.
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
1. Fork it
|
|
157
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
158
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
159
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
160
|
+
5. Create new Pull Request
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
module WtwUk
|
|
2
|
+
# The object representing a cinema on the WTW Cinemas website
|
|
3
|
+
class Cinema < Cinebase::Cinema
|
|
4
|
+
# @!attribute [r] id
|
|
5
|
+
# @return [String] the venue's TapOS site code, e.g. 'PLATRU'
|
|
6
|
+
|
|
7
|
+
# Each venue is a subsite, and its programme request names it by Jacro
|
|
8
|
+
# cinema id. Neither page names the TapOS site code its booking pages
|
|
9
|
+
# use, and no page gives coordinates, so the gem holds the venues: names,
|
|
10
|
+
# subsites and cinema ids from the quick booking menu, codes from booking
|
|
11
|
+
# pages, addresses from venue page footers and coordinates from
|
|
12
|
+
# OpenStreetMap.
|
|
13
|
+
VENUES = {
|
|
14
|
+
'WTWNEW' => {
|
|
15
|
+
name: 'Newquay', path: 'newquay', location: '5', street_address: 'King Street', locality: 'Newquay',
|
|
16
|
+
region: 'Cornwall', postal_code: 'TR7 1AA', latitude: 50.4136279, longitude: -5.0873384
|
|
17
|
+
},
|
|
18
|
+
'FILAUS' => {
|
|
19
|
+
name: 'St Austell', path: 'st-austell', location: '1', street_address: 'White River Place',
|
|
20
|
+
locality: 'St Austell', region: 'Cornwall', postal_code: 'PL25 5AZ', latitude: 50.3376527,
|
|
21
|
+
longitude: -4.7948318
|
|
22
|
+
},
|
|
23
|
+
'PLATRU' => {
|
|
24
|
+
name: 'Truro', path: 'truro', location: '2', street_address: '69 Lemon Street', locality: 'Truro',
|
|
25
|
+
region: 'Cornwall', postal_code: 'TR1 2PN', latitude: 50.261851, longitude: -5.0523149
|
|
26
|
+
},
|
|
27
|
+
'REGWAD' => {
|
|
28
|
+
name: 'Wadebridge', path: 'wadebridge', location: '3', street_address: 'The Platt', locality: 'Wadebridge',
|
|
29
|
+
region: 'Cornwall', postal_code: 'PL27 7AD', latitude: 50.5152411, longitude: -4.8361756
|
|
30
|
+
}
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
ADDRESS_PARTS = %i(street_address extended_address locality region postal_code).freeze
|
|
34
|
+
WEBSITE = 'https://wtwcinemas.co.uk'.freeze
|
|
35
|
+
|
|
36
|
+
# Constructor
|
|
37
|
+
# @param [String] id the venue's TapOS site code, e.g. 'PLATRU' or 'platru'
|
|
38
|
+
# @return [WtwUk::Cinema]
|
|
39
|
+
def initialize(id)
|
|
40
|
+
@id = id.to_s.upcase
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Return basic cinema information for all cinemas
|
|
44
|
+
# @return [Array<WtwUk::Cinema>]
|
|
45
|
+
# @example
|
|
46
|
+
# WtwUk::Cinema.all
|
|
47
|
+
# #=> [<WtwUk::Cinema>, <WtwUk::Cinema>, ...]
|
|
48
|
+
def self.all
|
|
49
|
+
VENUES.keys.map { |id| new(id) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @!method address
|
|
53
|
+
# Address of the cinema
|
|
54
|
+
# @return [Hash] of different address parts
|
|
55
|
+
# @see #adr
|
|
56
|
+
|
|
57
|
+
# Address of the cinema
|
|
58
|
+
# @return [Hash] contains :street_address, :extended_address,
|
|
59
|
+
# :locality, :region, :postal_code, :country_name
|
|
60
|
+
# @example
|
|
61
|
+
# cinema = WtwUk::Cinema.new('PLATRU')
|
|
62
|
+
# cinema.adr
|
|
63
|
+
# #=> {
|
|
64
|
+
# street_address: '69 Lemon Street',
|
|
65
|
+
# extended_address: nil,
|
|
66
|
+
# locality: 'Truro',
|
|
67
|
+
# region: 'Cornwall',
|
|
68
|
+
# postal_code: 'TR1 2PN',
|
|
69
|
+
# country_name: 'United Kingdom'
|
|
70
|
+
# }
|
|
71
|
+
# @note Uses method naming as at http://microformats.org/wiki/adr
|
|
72
|
+
def adr
|
|
73
|
+
ADDRESS_PARTS.to_h { |part| [part, venue[part]] }.merge(country_name: 'United Kingdom')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Brand of the cinema
|
|
77
|
+
# @return [String] which will always be 'WTW Cinemas'
|
|
78
|
+
def brand
|
|
79
|
+
'WTW Cinemas'.freeze
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Float, nil]
|
|
83
|
+
def latitude
|
|
84
|
+
venue[:latitude]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Jacro's cinema id for the venue
|
|
88
|
+
# @return [String, nil]
|
|
89
|
+
# @api private
|
|
90
|
+
def location
|
|
91
|
+
venue[:location]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [Float, nil]
|
|
95
|
+
def longitude
|
|
96
|
+
venue[:longitude]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The name of the cinema
|
|
100
|
+
# @return [String]
|
|
101
|
+
# @example
|
|
102
|
+
# WtwUk::Cinema.new('FILAUS').name
|
|
103
|
+
# #=> 'St Austell'
|
|
104
|
+
def name
|
|
105
|
+
venue[:name].to_s
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The venue's subsite on the WTW Cinemas website
|
|
109
|
+
# @return [String, nil]
|
|
110
|
+
# @api private
|
|
111
|
+
def path
|
|
112
|
+
venue[:path]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The url of the cinema's pages on the WTW Cinemas website
|
|
116
|
+
# @return [String]
|
|
117
|
+
# @example
|
|
118
|
+
# WtwUk::Cinema.new('PLATRU').url
|
|
119
|
+
# #=> 'https://wtwcinemas.co.uk/truro/'
|
|
120
|
+
def url
|
|
121
|
+
path ? "#{WEBSITE}/#{path}/" : ''
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def venue
|
|
127
|
+
VENUES.fetch(id, {})
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
|
|
5
|
+
module WtwUk
|
|
6
|
+
# Internal utility classes: Do not use
|
|
7
|
+
# @api private
|
|
8
|
+
module Internal
|
|
9
|
+
# Requests to wtwcinemas.co.uk
|
|
10
|
+
module Http
|
|
11
|
+
RETRIES = 2
|
|
12
|
+
TRANSIENT_ERRORS = [
|
|
13
|
+
EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def post(url, form)
|
|
19
|
+
uri = URI(url)
|
|
20
|
+
request = Net::HTTP::Post.new(uri)
|
|
21
|
+
request.set_form_data(form)
|
|
22
|
+
attempts = 0
|
|
23
|
+
begin
|
|
24
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30) do |http|
|
|
25
|
+
http.request(request)
|
|
26
|
+
end
|
|
27
|
+
rescue *TRANSIENT_ERRORS
|
|
28
|
+
attempts += 1
|
|
29
|
+
raise if attempts > RETRIES
|
|
30
|
+
|
|
31
|
+
sleep(attempts)
|
|
32
|
+
retry
|
|
33
|
+
end
|
|
34
|
+
# Omit response bodies from errors, and never follow redirects.
|
|
35
|
+
raise "WTW Cinemas HTTP error #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
36
|
+
|
|
37
|
+
String.new(response.body, encoding: Encoding::UTF_8)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WtwUk
|
|
4
|
+
# @api private
|
|
5
|
+
module Internal
|
|
6
|
+
module Parser
|
|
7
|
+
# Reads a venue's whole programme: a block for each film, with its
|
|
8
|
+
# showtimes in order under date headings such as
|
|
9
|
+
# '<strong>Wednesday </strong>16 December 2026'
|
|
10
|
+
class Programme
|
|
11
|
+
FILM = '<div class="poster-case '
|
|
12
|
+
TITLE = %r{<span class="poster-title">([^<]*)</span>}
|
|
13
|
+
DATE = %r{dateshow"><strong>[^<]*</strong>\s*(\d{1,2} [A-Z][a-z]+ \d{4})\s*</span>}
|
|
14
|
+
SHOWTIME = %r{<div class="singlefilmperfs ([^"]*)">(.*?)<input type="hidden" class="perfcat_message"}m
|
|
15
|
+
|
|
16
|
+
# @param [String] html a venue's programme
|
|
17
|
+
def initialize(html)
|
|
18
|
+
@html = html
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Array<WtwUk::Internal::Parser::Showtime>] every showtime,
|
|
22
|
+
# including those already passed today
|
|
23
|
+
def showtimes
|
|
24
|
+
@html.split(FILM).drop(1).flat_map do |markup|
|
|
25
|
+
title = markup[TITLE, 1]
|
|
26
|
+
title ? film_showtimes(CGI.unescapeHTML(title), markup) : []
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def film_showtimes(title, markup)
|
|
33
|
+
date = nil
|
|
34
|
+
markup.scan(/#{DATE}|#{SHOWTIME}/o).filter_map do |day, classes, perf|
|
|
35
|
+
if day
|
|
36
|
+
date = Date.strptime(day, '%d %B %Y')
|
|
37
|
+
nil
|
|
38
|
+
elsif date
|
|
39
|
+
Showtime.new(perf, title: title, date: date, classes: classes)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WtwUk
|
|
4
|
+
# @api private
|
|
5
|
+
module Internal
|
|
6
|
+
module Parser
|
|
7
|
+
# Parses one showtime from a venue's programme into performance
|
|
8
|
+
# attributes
|
|
9
|
+
class Showtime
|
|
10
|
+
# Showtime classes and the variants they mean. Others, such as
|
|
11
|
+
# 'audio-des' and 'ccap' (headsets and caption glasses on offer),
|
|
12
|
+
# 'sub-titles' (also on every opera), 'laser', 'strobe-lgt' and
|
|
13
|
+
# 'percatediscountedscreening' (cheap tickets), are not variants.
|
|
14
|
+
CLASSES = {
|
|
15
|
+
'aslive' => 'arts',
|
|
16
|
+
'aut' => 'relaxed',
|
|
17
|
+
'percateeventcinemascreening' => 'arts',
|
|
18
|
+
'sil' => 'senior',
|
|
19
|
+
'subtitled' => 'subtitled'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Event cinema classes are missing from some showtimes
|
|
23
|
+
TITLE_VARIANTS = {
|
|
24
|
+
/\b(?:Exhibition On Screen|National Theatre Live|NT Live|Royal Ballet (?:&|and) Opera|Live On Stage)\b/i =>
|
|
25
|
+
'arts',
|
|
26
|
+
/\s-\sThe Play\z/i => 'arts',
|
|
27
|
+
/\bQ&A\b/i => 'q&a',
|
|
28
|
+
/\bsubtitle[ds]\b/i => 'subtitled'
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
THREE_D_CLASS = 'icon-3d'
|
|
32
|
+
THREE_D = /\b3D\b/i
|
|
33
|
+
START = %r{Start time:<span>(\d{1,2}):(\d{2})</span>}
|
|
34
|
+
BOOKING = %r{href="(https://wtwcinemas\.co\.uk/[a-z-]+/booknow/\d+)/?"}
|
|
35
|
+
# Venues differ over hyphens and en dashes in the same title
|
|
36
|
+
EN_DASH = ' – '
|
|
37
|
+
LANGUAGE = /\((#{Cinebase::TitleSanitizer::LANGUAGES.join('|')})\)/i
|
|
38
|
+
TIME_ZONE = 'Europe/London'
|
|
39
|
+
|
|
40
|
+
# @return [Date] the local date of the showtime
|
|
41
|
+
attr_reader :date
|
|
42
|
+
|
|
43
|
+
# @param [String] markup the showtime's markup
|
|
44
|
+
# @param [String] title the film's title as listed
|
|
45
|
+
# @param [Date] date from the showtime's date heading
|
|
46
|
+
# @param [String] classes the showtime's class attribute
|
|
47
|
+
def initialize(markup, title:, date:, classes:)
|
|
48
|
+
@markup = markup
|
|
49
|
+
@title = title.gsub(EN_DASH, ' - ').strip
|
|
50
|
+
@date = date
|
|
51
|
+
@classes = classes.downcase.split
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Hash] options for WtwUk::Performance.new
|
|
55
|
+
def to_hash
|
|
56
|
+
{
|
|
57
|
+
booking_url: booking_url,
|
|
58
|
+
dimension: @classes.include?(THREE_D_CLASS) || @title.match?(THREE_D) ? '3d' : '2d',
|
|
59
|
+
festival: Cinebase::Festival.from_title(@title, year: @date.year),
|
|
60
|
+
film_name: TitleSanitizer.new(@title).sanitized,
|
|
61
|
+
film_year: Cinebase::TitleSanitizer.film_year(@title),
|
|
62
|
+
language: @title[LANGUAGE, 1],
|
|
63
|
+
starting_at: starting_at,
|
|
64
|
+
variant: variant
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Showtimes already passed have no link
|
|
71
|
+
def booking_url
|
|
72
|
+
url = @markup[BOOKING, 1]
|
|
73
|
+
url && "#{url}/"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def starting_at
|
|
77
|
+
hour, minute = @markup.match(START).captures.map(&:to_i)
|
|
78
|
+
local = Time.utc(@date.year, @date.month, @date.day, hour, minute)
|
|
79
|
+
TZInfo::Timezone.get(TIME_ZONE).local_to_utc(local) { |periods| periods.first }.utc
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def variant
|
|
83
|
+
from_title = TITLE_VARIANTS.filter_map { |pattern, variant| variant if @title.match?(pattern) }
|
|
84
|
+
(@classes.filter_map { |name| CLASSES[name] } + from_title).uniq
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module WtwUk
|
|
2
|
+
# Internal utility classes: Do not use
|
|
3
|
+
# @api private
|
|
4
|
+
module Internal
|
|
5
|
+
# Sanitize and standardize film titles
|
|
6
|
+
class TitleSanitizer < Cinebase::TitleSanitizer
|
|
7
|
+
# @!method initialize(title)
|
|
8
|
+
# Constructor
|
|
9
|
+
# @param [String] title a film title
|
|
10
|
+
# @return [WtwUk::Internal::TitleSanitizer]
|
|
11
|
+
|
|
12
|
+
# @!method sanitized
|
|
13
|
+
# sanitized and standardized title
|
|
14
|
+
# @return [String] sanitised title
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# strings and regex to be removed
|
|
19
|
+
def remove
|
|
20
|
+
[
|
|
21
|
+
/\ASee It First\s+-\s+/i, # preview
|
|
22
|
+
/\s*-\s*Pat's [^-]+\z/, # Truro's film club strands
|
|
23
|
+
/\s+(?:-\s+)?(?:Subtitled and )?Autism Friendly Screening\z/i, # relaxed screening
|
|
24
|
+
/\s+(?:inc\.?|plus)\s+(?:[\w-]+\s+)*Q&A\z/i # Q&A add-on
|
|
25
|
+
] + COMMON_REMOVE
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# regexes and their replacements
|
|
29
|
+
def replace
|
|
30
|
+
{
|
|
31
|
+
/\A(.*) - Royal Ballet (?:&|and) Opera \d{2}-\d{2} Season\z/i => 'Royal Ballet and Opera: ',
|
|
32
|
+
/\A(.*) - Exhibition On Screen\z/i => 'Exhibition on Screen: ',
|
|
33
|
+
/\ANT Live: (.*)/ => 'National Theatre Live: '
|
|
34
|
+
}.merge(COMMON_REPLACE)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WtwUk
|
|
4
|
+
# Internal utility classes: Do not use
|
|
5
|
+
# @api private
|
|
6
|
+
module Internal
|
|
7
|
+
# Reads WTW's WordPress website, built on Jacro's cinema plugin, with a
|
|
8
|
+
# subsite for each venue. No token is needed.
|
|
9
|
+
class Website
|
|
10
|
+
include Http
|
|
11
|
+
|
|
12
|
+
HOME = 'https://wtwcinemas.co.uk'
|
|
13
|
+
# The what's on page's request when a date is chosen
|
|
14
|
+
ACTION = 'jacro_filter_result'
|
|
15
|
+
# The 'All Shows' date, which lists every showtime on sale
|
|
16
|
+
ALL_DATES = 'moredates'
|
|
17
|
+
FILM_TYPE = 'Now Showing'
|
|
18
|
+
|
|
19
|
+
# @param [String] path the venue's subsite, e.g. 'truro'
|
|
20
|
+
# @param [String] location Jacro's cinema id for the venue, e.g. '2'
|
|
21
|
+
# @return [String] every film the venue has on sale, each with every
|
|
22
|
+
# showtime under date headings, however far ahead
|
|
23
|
+
def programme(path, location)
|
|
24
|
+
form = { 'action' => ACTION, 'film_date' => ALL_DATES, 'film_type' => FILM_TYPE, 'cinema_id' => location }
|
|
25
|
+
JSON.parse(post("#{HOME}/#{path}/wp-admin/admin-ajax.php", form)).fetch('html', '').to_s
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module WtwUk
|
|
2
|
+
# A single screening of a film on the WTW Cinemas website
|
|
3
|
+
class Performance < Cinebase::Performance
|
|
4
|
+
# @!attribute [r] booking_url
|
|
5
|
+
# @return [String, nil] the booking URL on the cinema website
|
|
6
|
+
# @!attribute [r] cinema_name
|
|
7
|
+
# @return [String] the cinema name
|
|
8
|
+
# @!attribute [r] cinema_id
|
|
9
|
+
# @return [String] the cinema id
|
|
10
|
+
# @!attribute [r] dimension
|
|
11
|
+
# @return [String] 2d or 3d
|
|
12
|
+
# @!attribute [r] film_name
|
|
13
|
+
# @return [String] the film name
|
|
14
|
+
|
|
15
|
+
# @!method initialize(options)
|
|
16
|
+
# @param [Hash] options options hash
|
|
17
|
+
# @option options [String] :booking_url (nil) buying url for the screening
|
|
18
|
+
# @option options [String] :cinema_name name of the cinema
|
|
19
|
+
# @option options [String] :cinema_id website id of the cinema
|
|
20
|
+
# @option options [String] :dimension ('2d') dimension of the screening
|
|
21
|
+
# @option options [Cinebase::Festival] :festival (nil) festival edition
|
|
22
|
+
# @option options [String] :film_name name of the film
|
|
23
|
+
# @option options [Integer] :film_year (nil) year from the film's title
|
|
24
|
+
# @option options [String] :language (nil) audio language, e.g. 'Hindi'
|
|
25
|
+
# @option options [Time] :starting_at listed start time of the performance
|
|
26
|
+
|
|
27
|
+
TIME_ZONE = 'Europe/London'.freeze
|
|
28
|
+
|
|
29
|
+
# Performances within an inclusive range of showing dates, from one
|
|
30
|
+
# request: the venue's programme holds every advertised date.
|
|
31
|
+
# @param [String] cinema_id the venue's TapOS site code, e.g. 'PLATRU'
|
|
32
|
+
# @param [Date] from first date (defaults to today in London)
|
|
33
|
+
# @param [Date] to last date (defaults to 13 days after from)
|
|
34
|
+
# @return [Array<WtwUk::Performance>]
|
|
35
|
+
def self.at(
|
|
36
|
+
cinema_id,
|
|
37
|
+
from: TZInfo::Timezone.get(TIME_ZONE).now.to_date,
|
|
38
|
+
to: from + 13
|
|
39
|
+
)
|
|
40
|
+
raise ArgumentError, 'to must not precede from' if to < from
|
|
41
|
+
|
|
42
|
+
cinema = Cinema.new(cinema_id)
|
|
43
|
+
if cinema.name.empty?
|
|
44
|
+
[]
|
|
45
|
+
else
|
|
46
|
+
html = WtwUk::Internal::Website.new.programme(cinema.path, cinema.location)
|
|
47
|
+
showtimes = WtwUk::Internal::Parser::Programme.new(html).showtimes
|
|
48
|
+
performances = showtimes.select { |showtime| (from..to).cover?(showtime.date) }.map do |showtime|
|
|
49
|
+
new(showtime.to_hash.merge(cinema_id: cinema.id, cinema_name: cinema.name))
|
|
50
|
+
end
|
|
51
|
+
performances.sort_by(&:starting_at)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @!method showing_on
|
|
56
|
+
# The date of the screening
|
|
57
|
+
# @return [Date]
|
|
58
|
+
|
|
59
|
+
# @!method starting_at
|
|
60
|
+
# UTC time of the screening
|
|
61
|
+
# @return [Time]
|
|
62
|
+
|
|
63
|
+
# @!method variant
|
|
64
|
+
# The kinds of screening (arts, q&a, relaxed, senior, subtitled)
|
|
65
|
+
# @return <Array[String]>
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/wtw_uk.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'cgi/escape'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
require 'cinebase'
|
|
5
|
+
|
|
6
|
+
require_relative './wtw_uk/version'
|
|
7
|
+
|
|
8
|
+
require_relative './wtw_uk/internal/http'
|
|
9
|
+
require_relative './wtw_uk/internal/website'
|
|
10
|
+
require_relative './wtw_uk/internal/title_sanitizer'
|
|
11
|
+
require_relative './wtw_uk/internal/parser/showtime'
|
|
12
|
+
require_relative './wtw_uk/internal/parser/programme'
|
|
13
|
+
|
|
14
|
+
require_relative './wtw_uk/cinema'
|
|
15
|
+
require_relative './wtw_uk/performance'
|
|
16
|
+
|
|
17
|
+
# wtw_uk gem
|
|
18
|
+
module WtwUk
|
|
19
|
+
end
|