picturehouse_uk 6.0.0-aarch64-linux-gnu
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/.github/workflows/ci.yml +47 -0
- data/.github/workflows/release.yml +73 -0
- data/.gitignore +18 -0
- data/.ruby-version +1 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +213 -0
- data/COMM-LICENSE +22 -0
- data/Gemfile +4 -0
- data/LICENSE +617 -0
- data/README.md +175 -0
- data/Rakefile +37 -0
- data/lib/picturehouse_uk/cinema.rb +127 -0
- data/lib/picturehouse_uk/internal/api_response.rb +35 -0
- data/lib/picturehouse_uk/internal/guest_token.rb +28 -0
- data/lib/picturehouse_uk/internal/parser/api/showtime.rb +70 -0
- data/lib/picturehouse_uk/internal/title_sanitizer.rb +78 -0
- data/lib/picturehouse_uk/performance.rb +80 -0
- data/lib/picturehouse_uk/version.rb +6 -0
- data/lib/picturehouse_uk.rb +30 -0
- data/picturehouse_uk.gemspec +29 -0
- data/test/fixtures/api/showtimes-019.json +62 -0
- data/test/fixtures/api/sites.json +40 -0
- data/test/lib/picturehouse_uk/cinema_test.rb +85 -0
- data/test/lib/picturehouse_uk/internal/api_response_test.rb +52 -0
- data/test/lib/picturehouse_uk/internal/fixture_shape_test.rb +14 -0
- data/test/lib/picturehouse_uk/internal/guest_token_test.rb +71 -0
- data/test/lib/picturehouse_uk/internal/parser/api/showtime_test.rb +59 -0
- data/test/lib/picturehouse_uk/internal/title_sanitizer_test.rb +243 -0
- data/test/lib/picturehouse_uk/performance_test.rb +111 -0
- data/test/lib/picturehouse_uk/version_test.rb +7 -0
- data/test/live/api_shape_test.rb +19 -0
- data/test/live/integration_test.rb +27 -0
- data/test/live/live_helper.rb +11 -0
- data/test/package_test.rb +56 -0
- data/test/support/api_fixtures_helper.rb +17 -0
- data/test/support/api_shapes.rb +48 -0
- data/test/support/fake_api_response.rb +20 -0
- data/test/test_helper.rb +20 -0
- data/vendor/curl-impersonate/SOURCE +5 -0
- data/vendor/curl-impersonate/bin/curl-impersonate +0 -0
- data/vendor/curl-impersonate/licenses/LICENSE +21 -0
- data/vendor/curl-impersonate/licenses/LICENSE_BORINGSSL +238 -0
- data/vendor/curl-impersonate/licenses/LICENSE_BROTLI +19 -0
- data/vendor/curl-impersonate/licenses/LICENSE_CARES +24 -0
- data/vendor/curl-impersonate/licenses/LICENSE_CURL +22 -0
- data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_GPL2 +339 -0
- data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_GPL3 +35 -0
- data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_LGPL3 +165 -0
- data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_UNICODE +91 -0
- data/vendor/curl-impersonate/licenses/LICENSE_NGHTTP2 +23 -0
- data/vendor/curl-impersonate/licenses/LICENSE_NGHTTP3 +22 -0
- data/vendor/curl-impersonate/licenses/LICENSE_NGTCP2 +22 -0
- data/vendor/curl-impersonate/licenses/LICENSE_ZLIB +22 -0
- data/vendor/curl-impersonate/licenses/LICENSE_ZSTD +30 -0
- metadata +225 -0
data/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# PicturehouseUk
|
|
2
|
+
|
|
3
|
+
A simple gem to pull cinemas and performances from the Vista API behind the [Picturehouse website](https://www.picturehouses.com) and spit out useful formatted info.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/picturehouse_uk)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
gem 'picturehouse_uk'
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install picturehouse_uk
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
Performance titles are sanitized as much as possible, removing 'screening type' information, years, Q&A add-ons and festival prefixes, and standardising theatre and opera event naming.
|
|
26
|
+
|
|
27
|
+
### Guest token
|
|
28
|
+
|
|
29
|
+
Every API call needs the public guest token the Picturehouse ticketing site
|
|
30
|
+
gives each visitor. Fetch one before anything else:
|
|
31
|
+
|
|
32
|
+
``` ruby
|
|
33
|
+
PicturehouseUk.acquire_token!
|
|
34
|
+
PicturehouseUk::Cinema.all
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This shells out to **`curl-impersonate`**, not ordinary system `curl`, using its
|
|
38
|
+
Chrome 150 TLS/HTTP profile, and reads `gasToken` from the page data of
|
|
39
|
+
`https://web.picturehouses.com/`. No browser or JavaScript execution is needed.
|
|
40
|
+
|
|
41
|
+
Platform-specific gem packages include the pinned `curl-impersonate` 2.2.3
|
|
42
|
+
executable for glibc Linux (x86-64, ARM64) and macOS (Intel, Apple Silicon).
|
|
43
|
+
On other platforms, or when installing from Git, install a
|
|
44
|
+
[curl-impersonate release](https://github.com/lexiforest/curl-impersonate/releases/tag/v2.2.3)
|
|
45
|
+
yourself and put it on your `PATH` or pass its path:
|
|
46
|
+
|
|
47
|
+
``` ruby
|
|
48
|
+
PicturehouseUk.acquire_token!(curl: '/opt/curl-impersonate/curl-impersonate')
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or supply your own current guest JWT, without the `Bearer ` prefix:
|
|
52
|
+
|
|
53
|
+
``` ruby
|
|
54
|
+
PicturehouseUk.token = ENV.fetch('PICTUREHOUSE_TOKEN')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Tokens currently last 12 hours. The gem checks the declared expiry but does not
|
|
58
|
+
verify the signature. Missing, expired or rejected tokens raise
|
|
59
|
+
`PicturehouseUk::AuthenticationError`; call `PicturehouseUk.acquire_token!`
|
|
60
|
+
again. Normal API calls never acquire or renew a token automatically.
|
|
61
|
+
|
|
62
|
+
### Cinema
|
|
63
|
+
|
|
64
|
+
Cinema ids are Vista site ids. Duke's at Komedia is `'019'`.
|
|
65
|
+
|
|
66
|
+
``` ruby
|
|
67
|
+
PicturehouseUk::Cinema.all
|
|
68
|
+
#=> [<PicturehouseUk::Cinema ...>, <PicturehouseUk::Cinema ...>, ...]
|
|
69
|
+
|
|
70
|
+
cinema = PicturehouseUk::Cinema.new(19)
|
|
71
|
+
#=> <PicturehouseUk::Cinema ...>
|
|
72
|
+
|
|
73
|
+
cinema.adr
|
|
74
|
+
#=> {
|
|
75
|
+
street_address: '44-47 Gardner Street',
|
|
76
|
+
extended_address: nil,
|
|
77
|
+
locality: 'Brighton',
|
|
78
|
+
region: nil,
|
|
79
|
+
postal_code: 'BN1 1UN',
|
|
80
|
+
country_name: 'United Kingdom'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cinema.brand
|
|
84
|
+
#=> 'Picturehouse'
|
|
85
|
+
|
|
86
|
+
cinema.name
|
|
87
|
+
#=> "Duke's at Komedia"
|
|
88
|
+
|
|
89
|
+
cinema.id
|
|
90
|
+
#=> '019'
|
|
91
|
+
|
|
92
|
+
cinema.url
|
|
93
|
+
#=> 'https://www.picturehouses.com/cinema/duke-s-at-komedia'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Performances
|
|
97
|
+
|
|
98
|
+
`Performance.at` returns performances for an inclusive range of business dates,
|
|
99
|
+
defaulting to today in London and the following 13 days. It makes one request
|
|
100
|
+
per day.
|
|
101
|
+
|
|
102
|
+
``` ruby
|
|
103
|
+
PicturehouseUk::Performance.at(19, from: Date.today, to: Date.today + 2)
|
|
104
|
+
#=> [<PicturehouseUk::Performance ...>, <PicturehouseUk::Performance ...>, ...]
|
|
105
|
+
|
|
106
|
+
performance = PicturehouseUk::Performance.at(19).first
|
|
107
|
+
#=> <PicturehouseUk::Performance ...>
|
|
108
|
+
|
|
109
|
+
performance.film_name
|
|
110
|
+
#=> 'Sense and Sensibility'
|
|
111
|
+
|
|
112
|
+
performance.film_year
|
|
113
|
+
#=> 2026 # from the title, nil when it has no year
|
|
114
|
+
|
|
115
|
+
performance.dimension
|
|
116
|
+
#=> '2d'
|
|
117
|
+
|
|
118
|
+
performance.variant
|
|
119
|
+
#=> ['audio_described', 'subtitled']
|
|
120
|
+
|
|
121
|
+
performance.subtitled?
|
|
122
|
+
#=> true
|
|
123
|
+
|
|
124
|
+
performance.festival
|
|
125
|
+
#=> #<data Cinebase::Festival name="CFF", year=2026> # nil outside festivals
|
|
126
|
+
|
|
127
|
+
performance.starting_at
|
|
128
|
+
#=> 2026-10-23 15:00:00 UTC
|
|
129
|
+
|
|
130
|
+
performance.booking_url
|
|
131
|
+
#=> 'https://web.picturehouses.com/order/showtimes/019-41632/seats'
|
|
132
|
+
|
|
133
|
+
performance.cinema_name
|
|
134
|
+
#=> "Duke's at Komedia"
|
|
135
|
+
|
|
136
|
+
performance.cinema_id
|
|
137
|
+
#=> '019'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Variants are `arts`, `audio_described`, `baby`, `kids`, `q&a`, `relaxed`,
|
|
141
|
+
`senior` and `subtitled`. Picturehouse doesn't tag audio languages, so
|
|
142
|
+
`language` is always `nil`.
|
|
143
|
+
|
|
144
|
+
## Tests
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
bundle exec rake # unit tests against fixtures
|
|
148
|
+
bundle exec rake live # live API, needs curl-impersonate or PICTUREHOUSE_TOKEN
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Fixtures in `test/fixtures/api` are hand-written: invented films and
|
|
152
|
+
addresses in the shape of Vista responses, never recorded API output.
|
|
153
|
+
`test/support/api_shapes.rb` describes the fields the gem reads. The unit suite
|
|
154
|
+
checks the fixtures against it and the live suite checks real responses, held
|
|
155
|
+
only in memory. When the API changes, update the shape first, then the fixtures.
|
|
156
|
+
|
|
157
|
+
## Building release packages
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
bundle exec rake build # Portable source gem; external curl-impersonate required
|
|
161
|
+
bundle exec rake package:all # Four platform gems with bundled executables
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
To release, bump the version, merge, then run the **Release Gem** workflow in
|
|
165
|
+
GitHub Actions. It builds the source and four platform gems, publishes them
|
|
166
|
+
through RubyGems trusted publishing (no MFA code needed) and tags the release.
|
|
167
|
+
Versions already on RubyGems are skipped, so a failed run can be rerun.
|
|
168
|
+
|
|
169
|
+
## Contributing
|
|
170
|
+
|
|
171
|
+
1. Fork it
|
|
172
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
173
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
174
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
175
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env rake
|
|
2
|
+
require 'bundler/gem_tasks'
|
|
3
|
+
|
|
4
|
+
require 'rake/testtask'
|
|
5
|
+
require 'cinebase/vista/package_task'
|
|
6
|
+
|
|
7
|
+
Cinebase::Vista::PackageTask.new('picturehouse_uk.gemspec', root: __dir__)
|
|
8
|
+
|
|
9
|
+
Rake::TestTask.new do |t|
|
|
10
|
+
t.libs << 'lib/picturehouse_uk'
|
|
11
|
+
t.test_files = FileList[
|
|
12
|
+
'test/lib/picturehouse_uk/*_test.rb',
|
|
13
|
+
'test/lib/picturehouse_uk/internal/*_test.rb',
|
|
14
|
+
'test/lib/picturehouse_uk/internal/parser/api/*_test.rb'
|
|
15
|
+
]
|
|
16
|
+
t.verbose = true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Rake::TestTask.new do |t|
|
|
20
|
+
t.libs << 'lib/picturehouse_uk'
|
|
21
|
+
t.name = :live
|
|
22
|
+
t.test_files = FileList[
|
|
23
|
+
'test/live/*_test.rb'
|
|
24
|
+
]
|
|
25
|
+
t.verbose = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# http://erniemiller.org/2014/02/05/7-lines-every-gems-rakefile-should-have/
|
|
29
|
+
task :console do
|
|
30
|
+
require 'irb'
|
|
31
|
+
require 'irb/completion'
|
|
32
|
+
require 'picturehouse_uk'
|
|
33
|
+
ARGV.clear
|
|
34
|
+
IRB.start
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
task default: :test
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
module PicturehouseUk
|
|
2
|
+
# The object representing a cinema on the Picturehouse UK website
|
|
3
|
+
class Cinema < Cinebase::Cinema
|
|
4
|
+
# @!attribute [r] id
|
|
5
|
+
# @return [String] the zero-padded Vista id of the cinema, e.g. '019'
|
|
6
|
+
|
|
7
|
+
# Vista site ids whose page slug differs from the cinema name
|
|
8
|
+
URL_SLUGS = {
|
|
9
|
+
'013' => 'picturehouse-at-fact',
|
|
10
|
+
'030' => 'epsom-picturehouse'
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
# UK postcode, the last line of an address
|
|
14
|
+
POSTCODE = /\A[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}\z/i.freeze
|
|
15
|
+
|
|
16
|
+
# Constructor
|
|
17
|
+
# @param [Integer, String] id the cinema id of the format 19/'019'
|
|
18
|
+
# @return [PicturehouseUk::Cinema]
|
|
19
|
+
def initialize(id)
|
|
20
|
+
@id = id.to_s.rjust(3, '0')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Return basic cinema information for all cinemas
|
|
24
|
+
# @return [Array<PicturehouseUk::Cinema>]
|
|
25
|
+
# @example
|
|
26
|
+
# PicturehouseUk::Cinema.all
|
|
27
|
+
# #=> [<PicturehouseUk::Cinema>, <PicturehouseUk::Cinema>, ...]
|
|
28
|
+
def self.all
|
|
29
|
+
sites_hash.keys.map { |id| new(id) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @!method address
|
|
33
|
+
# Address of the cinema
|
|
34
|
+
# @return [Hash] of different address parts
|
|
35
|
+
# @see #adr
|
|
36
|
+
|
|
37
|
+
# Address of the cinema
|
|
38
|
+
# @return [Hash] contains :street_address, :extended_address,
|
|
39
|
+
# :locality, :region, :postal_code, :country_name
|
|
40
|
+
# @example
|
|
41
|
+
# cinema = PicturehouseUk::Cinema.new(19)
|
|
42
|
+
# cinema.adr
|
|
43
|
+
# #=> {
|
|
44
|
+
# street_address: '44-47 Gardner Street',
|
|
45
|
+
# extended_address: nil,
|
|
46
|
+
# locality: 'Brighton',
|
|
47
|
+
# region: nil,
|
|
48
|
+
# postal_code: 'BN1 1UN',
|
|
49
|
+
# country_name: 'United Kingdom'
|
|
50
|
+
# }
|
|
51
|
+
# @note Uses method naming as at http://microformats.org/wiki/adr
|
|
52
|
+
def adr
|
|
53
|
+
lines = address_lines
|
|
54
|
+
postal_code = lines.pop if lines.last&.match?(POSTCODE)
|
|
55
|
+
street_address = lines.shift
|
|
56
|
+
locality = lines.pop
|
|
57
|
+
{
|
|
58
|
+
street_address: street_address,
|
|
59
|
+
extended_address: lines.empty? ? nil : lines.join(', '),
|
|
60
|
+
locality: locality,
|
|
61
|
+
region: nil,
|
|
62
|
+
postal_code: postal_code,
|
|
63
|
+
country_name: 'United Kingdom'.freeze
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Brand of the cinema
|
|
68
|
+
# @return [String] which will always be 'Picturehouse'
|
|
69
|
+
def brand
|
|
70
|
+
'Picturehouse'.freeze
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The name of the cinema, which often includes the brand
|
|
74
|
+
# @return [String]
|
|
75
|
+
# @example
|
|
76
|
+
# cinema = PicturehouseUk::Cinema.new(19)
|
|
77
|
+
# cinema.full_name
|
|
78
|
+
# #=> "Duke's at Komedia"
|
|
79
|
+
def full_name
|
|
80
|
+
name
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The name of the cinema
|
|
84
|
+
# @return [String]
|
|
85
|
+
# @example
|
|
86
|
+
# cinema = PicturehouseUk::Cinema.new(19)
|
|
87
|
+
# cinema.name
|
|
88
|
+
# #=> "Duke's at Komedia"
|
|
89
|
+
def name
|
|
90
|
+
site_hash.dig('name', 'text').to_s
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The url of the cinema on the Picturehouse website
|
|
94
|
+
# @return [String]
|
|
95
|
+
def url
|
|
96
|
+
"https://www.picturehouses.com/cinema/#{url_slug}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @api private
|
|
100
|
+
def self.sites_hash
|
|
101
|
+
@sites_hash ||= PicturehouseUk::Internal::ApiResponse.new.sites.to_h do |site|
|
|
102
|
+
[site.fetch('id'), site]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
private_class_method :sites_hash
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def address_lines
|
|
110
|
+
address = site_hash.dig('contactDetails', 'addressV2') || {}
|
|
111
|
+
%w(line1 line2 line3 line4).filter_map do |key|
|
|
112
|
+
text = address.dig(key, 'text').to_s.strip
|
|
113
|
+
text unless text.empty?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def site_hash
|
|
118
|
+
@site_hash ||= self.class.send(:sites_hash).fetch(id, {})
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def url_slug
|
|
122
|
+
URL_SLUGS.fetch(id) do
|
|
123
|
+
name.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-|-\z/, '')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module PicturehouseUk
|
|
6
|
+
# Internal utility classes: Do not use
|
|
7
|
+
# @api private
|
|
8
|
+
module Internal
|
|
9
|
+
# Fetches cinema and showtime data from the Vista API behind picturehouses.com
|
|
10
|
+
class ApiResponse
|
|
11
|
+
BASE_URL = "https://digital-api.picturehouses.com/ocapi/v1/"
|
|
12
|
+
|
|
13
|
+
# @return [Array<Hash>] cinemas
|
|
14
|
+
def sites
|
|
15
|
+
client.get("sites").fetch("sites")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param [String] cinema_id zero-padded site id, e.g. '019'
|
|
19
|
+
# @param [Date] date business date
|
|
20
|
+
# @return [Hash] showtimes plus their related films and attributes
|
|
21
|
+
def showtimes(cinema_id, date)
|
|
22
|
+
client.get("showtimes/by-business-date/#{date.iso8601}",
|
|
23
|
+
"siteIds" => cinema_id)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Picturehouse's API rejects every Connect-Region-Code, so send none.
|
|
29
|
+
def client
|
|
30
|
+
Cinebase::Vista::Client.new(base_url: BASE_URL, region_code: nil,
|
|
31
|
+
token: PicturehouseUk.token)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module PicturehouseUk
|
|
6
|
+
module Internal
|
|
7
|
+
# Reads the public guest token from the ticketing site's page data
|
|
8
|
+
class GuestToken < Cinebase::Vista::GuestToken
|
|
9
|
+
PAGE_URL = "https://web.picturehouses.com/"
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# The ticketing site serves the same page data for unknown paths.
|
|
14
|
+
def accepted_statuses
|
|
15
|
+
%w[200 404]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def bundled_curl
|
|
19
|
+
File.expand_path("../../../vendor/curl-impersonate/bin/curl-impersonate", __dir__)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def extract_token(html)
|
|
23
|
+
script = html[%r{<script id="__NEXT_DATA__"[^>]*>(.*?)</script>}m, 1].to_s
|
|
24
|
+
JSON.parse(script).dig("props", "pageProps", "environment", "gasToken")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module PicturehouseUk
|
|
6
|
+
# @api private
|
|
7
|
+
module Internal
|
|
8
|
+
module Parser
|
|
9
|
+
module Api
|
|
10
|
+
# Parses a single Vista showtime into performance attributes
|
|
11
|
+
class Showtime
|
|
12
|
+
BOOKING_URL = "https://web.picturehouses.com/order/showtimes/%s/seats"
|
|
13
|
+
|
|
14
|
+
# Vista attribute names, stripped
|
|
15
|
+
VARIANTS = {
|
|
16
|
+
"AD" => "audio_described",
|
|
17
|
+
"Kids' Club" => "kids",
|
|
18
|
+
"Q&A" => "q&a",
|
|
19
|
+
"Relaxed" => "relaxed",
|
|
20
|
+
"Screen Arts" => "arts",
|
|
21
|
+
"Silver Screen" => "senior",
|
|
22
|
+
"Subtitled" => "subtitled",
|
|
23
|
+
"Subtitled (Captioned)" => "subtitled",
|
|
24
|
+
"Toddler Time" => "kids",
|
|
25
|
+
"Watch with Baby" => "baby"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# @param [Hash] data one showtime
|
|
29
|
+
# @param [Hash{String => Hash}] films related films keyed by id
|
|
30
|
+
# @param [Hash{String => Hash}] attributes related attributes keyed by id
|
|
31
|
+
def initialize(data, films:, attributes:)
|
|
32
|
+
@data = data
|
|
33
|
+
@films = films
|
|
34
|
+
@attributes = attributes
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Hash] options for PicturehouseUk::Performance.new
|
|
38
|
+
def to_hash
|
|
39
|
+
{
|
|
40
|
+
booking_url: format(BOOKING_URL, @data.fetch("id")),
|
|
41
|
+
dimension: @data["requires3dGlasses"] ? "3d" : "2d",
|
|
42
|
+
festival: Cinebase::Festival.from_title(title, year: starting_at.year),
|
|
43
|
+
film_name: TitleSanitizer.new(title).sanitized,
|
|
44
|
+
film_year: Cinebase::TitleSanitizer.film_year(title),
|
|
45
|
+
starting_at: starting_at,
|
|
46
|
+
variant: variant
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def starting_at
|
|
53
|
+
Time.iso8601(@data.dig("schedule", "startsAt")).utc
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def title
|
|
57
|
+
@films.fetch(@data.fetch("filmId")).dig("title", "text")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def variant
|
|
61
|
+
@data.fetch("attributeIds", []).filter_map do |id|
|
|
62
|
+
name = @attributes.dig(id, "name", "text").to_s.strip
|
|
63
|
+
VARIANTS[name.delete_prefix("+ ")]
|
|
64
|
+
end.uniq
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module PicturehouseUk
|
|
2
|
+
# @api private
|
|
3
|
+
module Internal
|
|
4
|
+
# Sanitize and standardize film titles
|
|
5
|
+
class TitleSanitizer < Cinebase::TitleSanitizer
|
|
6
|
+
# @!method initialize(title)
|
|
7
|
+
# Constructor
|
|
8
|
+
# @param [String] title a film title
|
|
9
|
+
# @return [CineworldUk::Internal::TitleSanitizer]
|
|
10
|
+
|
|
11
|
+
# @!method sanitized
|
|
12
|
+
# sanitized and standardized title
|
|
13
|
+
# @return [String] sanitised title
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# strings and regex to be removed
|
|
18
|
+
def remove
|
|
19
|
+
[
|
|
20
|
+
/\s\[(AS LIVE:\s*)?[ACPGU1258]+\]/, # regular certificate
|
|
21
|
+
/\s\[R18\]/, # R18
|
|
22
|
+
/\s+\(?[23][dD]\)?/, # 2d or 3d from title
|
|
23
|
+
/\bIMAX\b/i, # imax from title
|
|
24
|
+
/\s\[NO CERT\]/, # no certificate
|
|
25
|
+
/\s\[TBC\]/, # tbc
|
|
26
|
+
/\s\[N\/A\]/i, # n/a
|
|
27
|
+
/\s\(Theatre\)/i, # (theatre)
|
|
28
|
+
/\s\[\]/, # blank certificate
|
|
29
|
+
/ourscreen\: /, # ourscreen
|
|
30
|
+
/\s\(Re(\: \d{0,4})?\)/i, # Re-release
|
|
31
|
+
/\s\[CERT TBC\]/, # certificate TBC
|
|
32
|
+
/\s?\-\s?autism.*ing\s?/i, # austim screening
|
|
33
|
+
/\s?\+\s?Q\&(amp;)*A\.?/i, # +Q&A
|
|
34
|
+
/KIDS CLUB\s*/i, # kids club
|
|
35
|
+
/DISCOVER TUE(sday\:)*\s*/i, # discover tue
|
|
36
|
+
/FREE Screening\s*-\s*/i, # free screening
|
|
37
|
+
/\s*-?\s*Big Scream\s*-?\s*/i, # big scream
|
|
38
|
+
/\*?HOH Subtitled\*?/i, # subtitled
|
|
39
|
+
/\s*\-?\s*Reminiscence/i, # reminiscence
|
|
40
|
+
/\s*\-?\s*\(?Re\-issue\)?/i, # reissue
|
|
41
|
+
/\s*-?\s*Kids\'? Club\s*-?\s*/, # kids club
|
|
42
|
+
/\s*plus Q\&A.*/i, # extended Q&A
|
|
43
|
+
/Cinemania\s*[\:\-]/i, # cinemania
|
|
44
|
+
/\@\s*komedia/i, # @ komedia
|
|
45
|
+
/\s*\+ panel.*/i, # panel
|
|
46
|
+
/toddler time\s*\:*\-*/i, # toddler time
|
|
47
|
+
/\(.*\d{4}\)/, # year or captured year
|
|
48
|
+
/\bsingalong\b/i, # singalong
|
|
49
|
+
/\s+\-?\s*Parents \& Babies/i, # parents and babies
|
|
50
|
+
/Subtitled\:*\s*/i, # subtitled
|
|
51
|
+
/\(?live\)?\z/i, # live
|
|
52
|
+
/amp\;/i,
|
|
53
|
+
/\ADog-Friendly - /, # dog friendly
|
|
54
|
+
/\ARelaxed-Screening: /, # relaxed screening
|
|
55
|
+
/\ALMF Presents: /, # LMF strand
|
|
56
|
+
/\AGreen Screen: /, # Green Screen strand
|
|
57
|
+
/\AFFQ: / # FFQ strand
|
|
58
|
+
] + COMMON_REMOVE
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# regexes and their replacements
|
|
62
|
+
def replace
|
|
63
|
+
{
|
|
64
|
+
/Met\.? Encore:\s*(.*)/ => 'Met Opera: ',
|
|
65
|
+
/Met\.? Opera:\s*(.*)/ => 'Met Opera: ',
|
|
66
|
+
/National Theatre:\s*(.*)/ => 'NT Live: ',
|
|
67
|
+
/NT Encore:\s*(.*)/ => 'NT Live: ',
|
|
68
|
+
/NT Live:\s*(.*)/ => 'NT Live: ',
|
|
69
|
+
/ROH\.? Live:\s*(.*)/ => 'Royal Opera House: ',
|
|
70
|
+
/ROH\.? Encore:\s*(.*)/ => 'Royal Opera House: ',
|
|
71
|
+
/RSC\.? Live:\s*(.*)/ => 'Royal Shakespeare Company: ',
|
|
72
|
+
/RSC\.? Encore:\s*(.*)/ => 'Royal Shakespeare Company: ',
|
|
73
|
+
/(.*) \(Bolshoi Ballet\)/ => 'Bolshoi Ballet: '
|
|
74
|
+
}.merge(COMMON_REPLACE)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module PicturehouseUk
|
|
2
|
+
# A single screening of a film on the Picturehouse UK website
|
|
3
|
+
class Performance < Cinebase::Performance
|
|
4
|
+
# @!attribute [r] booking_url
|
|
5
|
+
# @return [String] 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 [Time] :starting_at listed start time of the performance
|
|
25
|
+
|
|
26
|
+
# Performances within an inclusive range of cinema business dates.
|
|
27
|
+
# @param [Integer, String] cinema_id id of the cinema
|
|
28
|
+
# @param [Date] from first date (defaults to today in London)
|
|
29
|
+
# @param [Date] to last date (defaults to 13 days after from)
|
|
30
|
+
# @return [Array<PicturehouseUk::Performance>]
|
|
31
|
+
def self.at(
|
|
32
|
+
cinema_id,
|
|
33
|
+
from: TZInfo::Timezone.get('Europe/London').now.to_date,
|
|
34
|
+
to: from + 13
|
|
35
|
+
)
|
|
36
|
+
raise ArgumentError, 'to must not precede from' if to < from
|
|
37
|
+
|
|
38
|
+
api = PicturehouseUk::Internal::ApiResponse.new
|
|
39
|
+
cinema = Cinema.new(cinema_id)
|
|
40
|
+
(from..to).flat_map do |date|
|
|
41
|
+
performances_on(api, cinema, date)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @!method showing_on
|
|
46
|
+
# The date of the screening
|
|
47
|
+
# @return [Date]
|
|
48
|
+
|
|
49
|
+
# @!method starting_at
|
|
50
|
+
# UTC time of the screening
|
|
51
|
+
# @return [Time]
|
|
52
|
+
|
|
53
|
+
# @!method variant
|
|
54
|
+
# The kinds of screening (arts, baby, kids, senior, subtitled ...)
|
|
55
|
+
# @return <Array[String]>
|
|
56
|
+
|
|
57
|
+
# private
|
|
58
|
+
|
|
59
|
+
def self.performances_on(api, cinema, date)
|
|
60
|
+
response = api.showtimes(cinema.id, date)
|
|
61
|
+
related = response.fetch('relatedData')
|
|
62
|
+
films = by_id(related.fetch('films'))
|
|
63
|
+
attributes = by_id(related.fetch('attributes'))
|
|
64
|
+
|
|
65
|
+
response.fetch('showtimes').map do |data|
|
|
66
|
+
showtime = PicturehouseUk::Internal::Parser::Api::Showtime.new(
|
|
67
|
+
data, films: films, attributes: attributes
|
|
68
|
+
)
|
|
69
|
+
new(showtime.to_hash.merge(cinema_id: cinema.id,
|
|
70
|
+
cinema_name: cinema.name))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
private_class_method :performances_on
|
|
74
|
+
|
|
75
|
+
def self.by_id(items)
|
|
76
|
+
items.to_h { |item| [item.fetch('id'), item] }
|
|
77
|
+
end
|
|
78
|
+
private_class_method :by_id
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
require 'cinebase'
|
|
4
|
+
require 'cinebase/vista'
|
|
5
|
+
|
|
6
|
+
require_relative './picturehouse_uk/version'
|
|
7
|
+
|
|
8
|
+
require_relative './picturehouse_uk/internal/api_response'
|
|
9
|
+
require_relative './picturehouse_uk/internal/guest_token'
|
|
10
|
+
require_relative './picturehouse_uk/internal/title_sanitizer'
|
|
11
|
+
|
|
12
|
+
require_relative './picturehouse_uk/internal/parser/api/showtime'
|
|
13
|
+
|
|
14
|
+
require_relative './picturehouse_uk/cinema'
|
|
15
|
+
require_relative './picturehouse_uk/performance'
|
|
16
|
+
|
|
17
|
+
# picturehouse_uk gem
|
|
18
|
+
module PicturehouseUk
|
|
19
|
+
AuthenticationError = Cinebase::Vista::AuthenticationError
|
|
20
|
+
|
|
21
|
+
# A raw guest JWT, without the "Bearer " prefix. Never persisted by the gem.
|
|
22
|
+
class << self
|
|
23
|
+
attr_accessor :token
|
|
24
|
+
|
|
25
|
+
# Explicit acquisition/renewal; ordinary API calls never launch a subprocess.
|
|
26
|
+
def acquire_token!(curl: nil)
|
|
27
|
+
self.token = Internal::GuestToken.new(curl: curl).fetch
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|