odeon_uk 5.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/test.yml +43 -0
- data/.gitignore +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +132 -0
- data/COMM-LICENSE +22 -0
- data/Gemfile +4 -0
- data/LICENSE +617 -0
- data/README.md +174 -0
- data/Rakefile +38 -0
- data/lib/odeon_uk/cinema.rb +181 -0
- data/lib/odeon_uk/internal/api_response.rb +36 -0
- data/lib/odeon_uk/internal/guest_token.rb +23 -0
- data/lib/odeon_uk/internal/parser/api/showtime.rb +80 -0
- data/lib/odeon_uk/internal/title_sanitizer.rb +42 -0
- data/lib/odeon_uk/performance.rb +81 -0
- data/lib/odeon_uk/version.rb +6 -0
- data/lib/odeon_uk.rb +30 -0
- data/odeon_uk.gemspec +31 -0
- data/test/fixtures/api/showtimes-824.json +66 -0
- data/test/fixtures/api/sites.json +32 -0
- data/test/lib/odeon_uk/cinema_test.rb +113 -0
- data/test/lib/odeon_uk/internal/api_response_test.rb +66 -0
- data/test/lib/odeon_uk/internal/fixture_shape_test.rb +14 -0
- data/test/lib/odeon_uk/internal/guest_token_test.rb +84 -0
- data/test/lib/odeon_uk/internal/parser/api/showtime_test.rb +68 -0
- data/test/lib/odeon_uk/internal/title_sanitizer_test.rb +141 -0
- data/test/lib/odeon_uk/performance_test.rb +150 -0
- data/test/lib/odeon_uk/version_test.rb +7 -0
- data/test/live/api_shape_test.rb +19 -0
- data/test/live/integration_test.rb +21 -0
- data/test/live/live_helper.rb +11 -0
- data/test/package_test.rb +56 -0
- data/test/support/api_fixtures_helper.rb +16 -0
- data/test/support/api_shapes.rb +46 -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 +223 -0
data/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# OdeonUk
|
|
2
|
+
|
|
3
|
+
A simple gem to pull cinemas and performances from the Vista API behind the [ODEON UK website](https://www.odeon.co.uk) and spit out useful formatted info.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/odeon_uk)
|
|
6
|
+
[](http://inch-ci.org/github/andycroll/odeon_uk)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Add this line to your application's Gemfile:
|
|
11
|
+
|
|
12
|
+
gem 'odeon_uk'
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
|
|
16
|
+
$ bundle
|
|
17
|
+
|
|
18
|
+
Or install it yourself as:
|
|
19
|
+
|
|
20
|
+
$ gem install odeon_uk
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
Performance titles are sanitized as much as possible, removing 'screening type' information and 'dimension' as well as standardising all the theatre/cultural event naming (NT Live, Royal Ballet and Opera etc).
|
|
27
|
+
|
|
28
|
+
### Guest token
|
|
29
|
+
|
|
30
|
+
Every API call needs the public guest token the ODEON website gives each
|
|
31
|
+
visitor. Fetch one before anything else:
|
|
32
|
+
|
|
33
|
+
``` ruby
|
|
34
|
+
OdeonUk.acquire_token!
|
|
35
|
+
OdeonUk::Cinema.all
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This shells out to **`curl-impersonate`**, not ordinary system `curl`, using its
|
|
39
|
+
Chrome 150 TLS/HTTP profile, and reads `window.initialData.api.authToken` from
|
|
40
|
+
`https://www.odeon.co.uk/my-account/`. That page skips the queue-it waiting room
|
|
41
|
+
every other page redirects to. No browser or JavaScript execution is needed.
|
|
42
|
+
|
|
43
|
+
Platform-specific gem packages include the pinned `curl-impersonate` 2.2.3
|
|
44
|
+
executable for glibc Linux (x86-64, ARM64) and macOS (Intel, Apple Silicon).
|
|
45
|
+
On other platforms, or when installing from Git, install a
|
|
46
|
+
[curl-impersonate release](https://github.com/lexiforest/curl-impersonate/releases/tag/v2.2.3)
|
|
47
|
+
yourself and put it on your `PATH` or pass its path:
|
|
48
|
+
|
|
49
|
+
``` ruby
|
|
50
|
+
OdeonUk.acquire_token!(curl: '/opt/curl-impersonate/curl-impersonate')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or supply your own current guest JWT, without the `Bearer ` prefix:
|
|
54
|
+
|
|
55
|
+
``` ruby
|
|
56
|
+
OdeonUk.token = ENV.fetch('ODEON_TOKEN')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Tokens currently last 12 hours. The gem checks the declared expiry but does not
|
|
60
|
+
verify the signature. Missing, expired or rejected tokens raise
|
|
61
|
+
`OdeonUk::AuthenticationError`; call `OdeonUk.acquire_token!` again. Normal API
|
|
62
|
+
calls never acquire or renew a token automatically.
|
|
63
|
+
|
|
64
|
+
### Cinema
|
|
65
|
+
|
|
66
|
+
Only UK cinemas are returned; ODEON's Irish cinemas are left out. Cinema ids
|
|
67
|
+
changed with the move to the Vista API (Brighton was 71 and is now 824). The API
|
|
68
|
+
does not supply postcodes, so `postal_code` is empty.
|
|
69
|
+
|
|
70
|
+
``` ruby
|
|
71
|
+
OdeonUk::Cinema.all
|
|
72
|
+
#=> [<OdeonUk::Cinema ...>, <OdeonUk::Cinema ...>, ...]
|
|
73
|
+
|
|
74
|
+
cinema = OdeonUk::Cinema.new(824)
|
|
75
|
+
#=> <OdeonUk::Cinema ...>
|
|
76
|
+
|
|
77
|
+
cinema.adr
|
|
78
|
+
#=> {
|
|
79
|
+
street_address: 'Kingswest',
|
|
80
|
+
extended_address: 'West Street',
|
|
81
|
+
locality: 'Brighton',
|
|
82
|
+
region: nil,
|
|
83
|
+
postal_code: nil,
|
|
84
|
+
country_name: 'United Kingdom'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
cinema.brand
|
|
88
|
+
#=> 'Odeon'
|
|
89
|
+
|
|
90
|
+
cinema.full_name
|
|
91
|
+
#=> 'Odeon Brighton'
|
|
92
|
+
|
|
93
|
+
cinema.id
|
|
94
|
+
#=> '824'
|
|
95
|
+
|
|
96
|
+
cinema.url
|
|
97
|
+
#=> 'https://www.odeon.co.uk/cinemas/brighton/'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Performances
|
|
101
|
+
|
|
102
|
+
`Performance.at` returns performances for an inclusive range of business dates,
|
|
103
|
+
defaulting to today in London and the following 13 days. It makes one request per
|
|
104
|
+
day.
|
|
105
|
+
|
|
106
|
+
``` ruby
|
|
107
|
+
OdeonUk::Performance.at(824)
|
|
108
|
+
#=> [<OdeonUk::Performance ...>, <OdeonUk::Performance ...>, ...]
|
|
109
|
+
|
|
110
|
+
OdeonUk::Performance.at(824, from: Date.today, to: Date.today + 2)
|
|
111
|
+
#=> [<OdeonUk::Performance ...>, <OdeonUk::Performance ...>, ...]
|
|
112
|
+
|
|
113
|
+
performance = OdeonUk::Performance.at(824).first
|
|
114
|
+
#=> <OdeonUk::Performance ...>
|
|
115
|
+
|
|
116
|
+
performance.film_name
|
|
117
|
+
#=> 'Street Fighter'
|
|
118
|
+
|
|
119
|
+
performance.dimension
|
|
120
|
+
#=> '2d'
|
|
121
|
+
|
|
122
|
+
performance.variant
|
|
123
|
+
#=> ['audio_described', 'imax']
|
|
124
|
+
|
|
125
|
+
performance.language
|
|
126
|
+
#=> 'Korean' # audio language, nil when the cinema does not tag one
|
|
127
|
+
|
|
128
|
+
performance.subtitled?
|
|
129
|
+
#=> true # English subtitles or open captions
|
|
130
|
+
|
|
131
|
+
performance.festival
|
|
132
|
+
#=> #<data Cinebase::Festival name="Cine-Excess", year=2026> # nil outside festivals
|
|
133
|
+
|
|
134
|
+
performance.starting_at
|
|
135
|
+
#=> 2026-10-24 08:00:00 UTC
|
|
136
|
+
|
|
137
|
+
performance.showing_on
|
|
138
|
+
#=> #<Date: 2026-10-24 ((2461338j,0s,0n),+0s,2299161j)>
|
|
139
|
+
|
|
140
|
+
performance.booking_url
|
|
141
|
+
#=> 'https://www.odeon.co.uk/ticketing/seat-picker/?showtimeId=824-69206'
|
|
142
|
+
|
|
143
|
+
performance.cinema_name
|
|
144
|
+
#=> 'Brighton'
|
|
145
|
+
|
|
146
|
+
performance.cinema_id
|
|
147
|
+
#=> '824'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Variants are `arts`, `audio_described`, `autism_friendly`, `baby`, `imax`,
|
|
151
|
+
`kids`, `senior` and `subtitled`.
|
|
152
|
+
|
|
153
|
+
## Tests
|
|
154
|
+
|
|
155
|
+
``` sh
|
|
156
|
+
bundle exec rake # unit tests against fixtures
|
|
157
|
+
bundle exec rake live # live API, needs curl-impersonate or ODEON_TOKEN
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Fixtures in `test/fixtures/api` are hand-written: invented films and
|
|
161
|
+
addresses in the shape of Vista responses, never recorded API output.
|
|
162
|
+
`test/support/api_shapes.rb` describes the fields the gem reads. The unit suite
|
|
163
|
+
checks the fixtures against it and the live suite checks real responses, held
|
|
164
|
+
only in memory. When the API changes, update the shape first, then the fixtures.
|
|
165
|
+
|
|
166
|
+
## Contributing
|
|
167
|
+
|
|
168
|
+
1. Fork it
|
|
169
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
170
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
171
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
172
|
+
5. Create new Pull Request
|
|
173
|
+
|
|
174
|
+
Note that contributors assign all rights to the owner, Andy Croll ([github](http://github.com/andycroll)), of this gem.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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('odeon_uk.gemspec', root: __dir__)
|
|
8
|
+
|
|
9
|
+
Rake::TestTask.new do |t|
|
|
10
|
+
t.libs << 'lib/odeon_uk'
|
|
11
|
+
t.test_files = FileList[
|
|
12
|
+
'test/lib/odeon_uk/*_test.rb',
|
|
13
|
+
'test/lib/odeon_uk/internal/*_test.rb',
|
|
14
|
+
'test/lib/odeon_uk/internal/parser/api/*_test.rb'
|
|
15
|
+
]
|
|
16
|
+
t.verbose = true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Rake::TestTask.new do |t|
|
|
20
|
+
t.libs << 'lib/odeon_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
|
+
desc 'run gem console'
|
|
30
|
+
task :console do
|
|
31
|
+
require 'irb'
|
|
32
|
+
require 'irb/completion'
|
|
33
|
+
require 'odeon_uk'
|
|
34
|
+
ARGV.clear
|
|
35
|
+
IRB.start
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
task default: :test
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
module OdeonUk
|
|
2
|
+
# The object representing a cinema on the Odeon UK website
|
|
3
|
+
class Cinema < Cinebase::Cinema
|
|
4
|
+
# @!attribute [r] id
|
|
5
|
+
# @return [String] the zero-padded id of the cinema, e.g. '017'
|
|
6
|
+
|
|
7
|
+
# Vista site ids whose page slug differs from the cinema name
|
|
8
|
+
URL_SLUGS = {
|
|
9
|
+
'915' => 'dudley-merry-hill',
|
|
10
|
+
'955' => 'manchester-trafford-centre'
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
# Constructor
|
|
14
|
+
# @param [Integer, String] id the cinema id of the format 17/'017'
|
|
15
|
+
# @return [OdeonUk::Cinema]
|
|
16
|
+
def initialize(id)
|
|
17
|
+
@id = id.to_s.rjust(3, '0')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Return basic cinema information for all UK cinemas
|
|
21
|
+
# @return [Array<OdeonUk::Cinema>]
|
|
22
|
+
# @example
|
|
23
|
+
# OdeonUk::Cinema.all
|
|
24
|
+
# #=> [<OdeonUk::Cinema>, <OdeonUk::Cinema>, ...]
|
|
25
|
+
def self.all
|
|
26
|
+
sites_hash.keys.map { |cinema_id| new(cinema_id) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @!method address
|
|
30
|
+
# Address of the cinema
|
|
31
|
+
# @return [Hash] of different address parts
|
|
32
|
+
# @see #adr
|
|
33
|
+
|
|
34
|
+
# Address of the cinema
|
|
35
|
+
# @return [Hash] contains :street_address, :extended_address,
|
|
36
|
+
# :locality, :postal_code, :country
|
|
37
|
+
# @example
|
|
38
|
+
# cinema = OdeonUk::Cinema.new(824)
|
|
39
|
+
# cinema.adr
|
|
40
|
+
# #=> {
|
|
41
|
+
# street_address: 'Kingswest',
|
|
42
|
+
# extended_address: 'West Street',
|
|
43
|
+
# locality: 'Brighton',
|
|
44
|
+
# region: nil,
|
|
45
|
+
# postal_code: nil,
|
|
46
|
+
# country_name: 'United Kingdom'
|
|
47
|
+
# }
|
|
48
|
+
# @note Uses the standard method naming as at http://microformats.org/wiki/adr
|
|
49
|
+
# @note The API does not supply postcodes
|
|
50
|
+
def adr
|
|
51
|
+
address = site_hash.dig('contactDetails', 'address') || {}
|
|
52
|
+
{
|
|
53
|
+
street_address: address['line1'],
|
|
54
|
+
extended_address: address['line2'],
|
|
55
|
+
locality: address['city'],
|
|
56
|
+
region: nil,
|
|
57
|
+
postal_code: nil,
|
|
58
|
+
country_name: 'United Kingdom'.freeze
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Brand of the cinema
|
|
63
|
+
# @return [String] which will always be 'Odeon'
|
|
64
|
+
# @example
|
|
65
|
+
# cinema = OdeonUk::Cinema.new(3)
|
|
66
|
+
# cinema.brand
|
|
67
|
+
# #=> 'Odeon'
|
|
68
|
+
def brand
|
|
69
|
+
'Odeon'.freeze
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @!method country_name
|
|
73
|
+
# Country of the cinema
|
|
74
|
+
# @return [String] which will always be 'United Kingdom'
|
|
75
|
+
# @example
|
|
76
|
+
# cinema = CineworldUk::Cinema.new(3)
|
|
77
|
+
# cinema.country_name
|
|
78
|
+
# #=> 'United Kingdom'
|
|
79
|
+
|
|
80
|
+
# @!method extended_address
|
|
81
|
+
# The second address line of the cinema
|
|
82
|
+
# @return [String]
|
|
83
|
+
# @example
|
|
84
|
+
# cinema = CineworldUk::Cinema.new(10)
|
|
85
|
+
# cinema.extended_address
|
|
86
|
+
# #=> 'Chelsea'
|
|
87
|
+
#
|
|
88
|
+
# cinema = CineworldUk::Cinema.new(3)
|
|
89
|
+
# cinema.extended_address
|
|
90
|
+
# #=> ''
|
|
91
|
+
|
|
92
|
+
# @!method full_name
|
|
93
|
+
# The name of the cinema including the brand
|
|
94
|
+
# @return [String]
|
|
95
|
+
# @example
|
|
96
|
+
# cinema = CineworldUk::Cinema.new(88)
|
|
97
|
+
# cinema.full_name
|
|
98
|
+
# #=> 'Cineworld Glasgow: IMAX at GSC'
|
|
99
|
+
|
|
100
|
+
# @!method locality
|
|
101
|
+
# The locality (town) of the cinema
|
|
102
|
+
# @return [String]
|
|
103
|
+
# @example
|
|
104
|
+
# cinema = CineworldUk::Cinema.new(3)
|
|
105
|
+
# cinema.locality
|
|
106
|
+
# #=> 'Brighton'
|
|
107
|
+
|
|
108
|
+
# The name of the cinema
|
|
109
|
+
# @return [String]
|
|
110
|
+
# @example
|
|
111
|
+
# cinema = CineworldUk::Cinema.new(3)
|
|
112
|
+
# cinema.name
|
|
113
|
+
# #=> 'Brighton'
|
|
114
|
+
def name
|
|
115
|
+
@name ||= site_name.gsub(/\ALondon /, '').gsub(' - ', ': ')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @!method postal_code
|
|
119
|
+
# Post code of the cinema
|
|
120
|
+
# @return [String]
|
|
121
|
+
# @example
|
|
122
|
+
# cinema = CineworldUk::Cinema.new(3)
|
|
123
|
+
# cinema.postal_code
|
|
124
|
+
# #=> 'BN2 5UF'
|
|
125
|
+
|
|
126
|
+
# @!method region
|
|
127
|
+
# The region (county) of the cinema if provided
|
|
128
|
+
# @return [String]
|
|
129
|
+
# @example
|
|
130
|
+
# cinema = CineworldUk::Cinema.new(3)
|
|
131
|
+
# cinema.region
|
|
132
|
+
# #=> 'East Sussex'
|
|
133
|
+
|
|
134
|
+
# @!method slug
|
|
135
|
+
# The URL-able slug of the cinema
|
|
136
|
+
# @return [String]
|
|
137
|
+
# @example
|
|
138
|
+
# cinema = CineworldUk::Cinema.new(3)
|
|
139
|
+
# cinema.slug
|
|
140
|
+
# #=> 'odeon-brighton'
|
|
141
|
+
|
|
142
|
+
# @!method street_address
|
|
143
|
+
# The street address of the cinema
|
|
144
|
+
# @return [String]
|
|
145
|
+
# @example
|
|
146
|
+
# cinema = CineworldUk::Cinema.new(3)
|
|
147
|
+
# cinema.street_address
|
|
148
|
+
# #=> 'Brighton Marina'
|
|
149
|
+
# @note Uses the standard method naming as at http://microformats.org/wiki/adr
|
|
150
|
+
|
|
151
|
+
# The url of the cinema on the Cineworld website
|
|
152
|
+
# @return [String]
|
|
153
|
+
def url
|
|
154
|
+
"https://www.odeon.co.uk/cinemas/#{url_slug}/"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @api private
|
|
158
|
+
def self.sites_hash
|
|
159
|
+
@sites_hash ||= OdeonUk::Internal::ApiResponse.new.sites.to_h do |site|
|
|
160
|
+
[site.fetch('id'), site]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
private_class_method :sites_hash
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def site_hash
|
|
168
|
+
@site_hash ||= self.class.send(:sites_hash).fetch(id, {})
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def site_name
|
|
172
|
+
site_hash.dig('name', 'text').to_s
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def url_slug
|
|
176
|
+
URL_SLUGS.fetch(id) do
|
|
177
|
+
site_name.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-|-\z/, '')
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module OdeonUk
|
|
6
|
+
# Internal utility classes: Do not use
|
|
7
|
+
# @api private
|
|
8
|
+
module Internal
|
|
9
|
+
# Fetches cinema and showtime data from the Vista API behind odeon.co.uk
|
|
10
|
+
class ApiResponse
|
|
11
|
+
BASE_URL = "https://vwc.odeon.co.uk/WSVistaWebClient/ocapi/v1/"
|
|
12
|
+
# Limits sites to the UK, leaving out ODEON's Irish cinemas.
|
|
13
|
+
REGION_CODE = "UK"
|
|
14
|
+
|
|
15
|
+
# @return [Array<Hash>] UK cinemas
|
|
16
|
+
def sites
|
|
17
|
+
client.get("sites").fetch("sites")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param [String] cinema_id zero-padded site id, e.g. '017'
|
|
21
|
+
# @param [Date] date business date
|
|
22
|
+
# @return [Hash] showtimes plus their related films and attributes
|
|
23
|
+
def showtimes(cinema_id, date)
|
|
24
|
+
client.get("showtimes/by-business-date/#{date.iso8601}",
|
|
25
|
+
"siteIds" => cinema_id)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def client
|
|
31
|
+
Cinebase::Vista::Client.new(base_url: BASE_URL, region_code: REGION_CODE,
|
|
32
|
+
token: OdeonUk.token)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OdeonUk
|
|
6
|
+
module Internal
|
|
7
|
+
# Reads the public guest token from the ODEON website's page data
|
|
8
|
+
class GuestToken < Cinebase::Vista::GuestToken
|
|
9
|
+
# Every other page redirects to the queue-it waiting room first.
|
|
10
|
+
PAGE_URL = "https://www.odeon.co.uk/my-account/"
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def bundled_curl
|
|
15
|
+
File.expand_path("../../../vendor/curl-impersonate/bin/curl-impersonate", __dir__)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_token(html)
|
|
19
|
+
JSON.parse(html[/window\.initialData\s*=\s*(.+?);\s*$/, 1].to_s).dig("api", "authToken")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module OdeonUk
|
|
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://www.odeon.co.uk/ticketing/seat-picker/?showtimeId="
|
|
13
|
+
|
|
14
|
+
# Vista attribute short names
|
|
15
|
+
VARIANTS = {
|
|
16
|
+
"AudioDes" => "audio_described",
|
|
17
|
+
"AutFriend" => "autism_friendly",
|
|
18
|
+
"EnglishSub" => "subtitled",
|
|
19
|
+
"EventCinem" => "arts",
|
|
20
|
+
"IMAX" => "imax",
|
|
21
|
+
"Newbies" => "baby",
|
|
22
|
+
"ODEON Kids" => "kids",
|
|
23
|
+
"OpenCapt" => "subtitled",
|
|
24
|
+
"SilverCine" => "senior"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# @param [Hash] data one showtime
|
|
28
|
+
# @param [Hash{String => Hash}] films related films keyed by id
|
|
29
|
+
# @param [Hash{String => Hash}] attributes related attributes keyed by id
|
|
30
|
+
def initialize(data, films:, attributes:)
|
|
31
|
+
@data = data
|
|
32
|
+
@films = films
|
|
33
|
+
@attributes = attributes
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Hash] options for OdeonUk::Performance.new
|
|
37
|
+
def to_hash
|
|
38
|
+
{
|
|
39
|
+
booking_url: BOOKING_URL + @data.fetch("id"),
|
|
40
|
+
dimension: @data["requires3dGlasses"] ? "3d" : "2d",
|
|
41
|
+
festival: Cinebase::Festival.from_title(title, year: starting_at.year),
|
|
42
|
+
film_name: film_name,
|
|
43
|
+
language: language,
|
|
44
|
+
starting_at: starting_at,
|
|
45
|
+
variant: variant
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def attribute_names(key)
|
|
52
|
+
@data.fetch("attributeIds", []).filter_map do |id|
|
|
53
|
+
@attributes.dig(id, key, "text")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def starting_at
|
|
58
|
+
Time.iso8601(@data.dig("schedule", "startsAt")).utc
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def film_name
|
|
62
|
+
TitleSanitizer.new(title).sanitized
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def title
|
|
66
|
+
@films.fetch(@data.fetch("filmId")).dig("title", "text")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def language
|
|
70
|
+
attribute_names("name").filter_map { |name| name[/\A(.+) \(Audio\)\z/, 1] }.first
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def variant
|
|
74
|
+
attribute_names("shortName").filter_map { |name| VARIANTS[name] }.uniq
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module OdeonUk
|
|
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 [CineworldUk::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
|
+
'Autism Friendly Screening -', # autism screening
|
|
22
|
+
/\(encore.+\)/i, # encore for NT Live
|
|
23
|
+
/\(live\)/i, # live in brackets
|
|
24
|
+
'Autism Friendly - ' # autism friendly
|
|
25
|
+
] + COMMON_REMOVE
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# regexes and their replacements
|
|
29
|
+
def replace
|
|
30
|
+
{
|
|
31
|
+
/Bolshoi - (.*)/ => 'Bolshoi: ',
|
|
32
|
+
/Globe On Screen: (.*)/ => 'Globe: ',
|
|
33
|
+
/Met Opera - (.*)/ => 'Met Opera: ',
|
|
34
|
+
/National Theatre Live - (.*)/ => 'National Theatre: ',
|
|
35
|
+
/NT Live - (.*)/ => 'National Theatre: ',
|
|
36
|
+
/ROH - (.*)/ => 'Royal Opera House: ',
|
|
37
|
+
/(.*)\- RSC Live \d{1,4}/ => 'Royal Shakespeare Company: '
|
|
38
|
+
}.merge(COMMON_REPLACE)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module OdeonUk
|
|
2
|
+
# The object representing a single screening of a film on the Odeon 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 [String] :film_name name of the film
|
|
22
|
+
# @option options [Time] :starting_at listed start time of the performance
|
|
23
|
+
# @option options [Cinebase::Festival] :festival (nil) festival edition
|
|
24
|
+
# @option options [String] :language (nil) audio language of the screening
|
|
25
|
+
|
|
26
|
+
# Performances within an inclusive range of cinema business dates.
|
|
27
|
+
# @param [Integer, String] cinema_id id of the cinema on the website
|
|
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<OdeonUk::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 = OdeonUk::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 (IMAX, kids, baby, senior)
|
|
55
|
+
# @return <Array[String]>
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# private
|
|
59
|
+
|
|
60
|
+
def self.performances_on(api, cinema, date)
|
|
61
|
+
response = api.showtimes(cinema.id, date)
|
|
62
|
+
related = response.fetch('relatedData')
|
|
63
|
+
films = by_id(related.fetch('films'))
|
|
64
|
+
attributes = by_id(related.fetch('attributes'))
|
|
65
|
+
|
|
66
|
+
response.fetch('showtimes').map do |data|
|
|
67
|
+
showtime = OdeonUk::Internal::Parser::Api::Showtime.new(
|
|
68
|
+
data, films: films, attributes: attributes
|
|
69
|
+
)
|
|
70
|
+
new(showtime.to_hash.merge(cinema_id: cinema.id,
|
|
71
|
+
cinema_name: cinema.name))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
private_class_method :performances_on
|
|
75
|
+
|
|
76
|
+
def self.by_id(items)
|
|
77
|
+
items.to_h { |item| [item.fetch('id'), item] }
|
|
78
|
+
end
|
|
79
|
+
private_class_method :by_id
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/odeon_uk.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
require 'cinebase'
|
|
4
|
+
require 'cinebase/vista'
|
|
5
|
+
|
|
6
|
+
require_relative './odeon_uk/version'
|
|
7
|
+
|
|
8
|
+
require_relative './odeon_uk/internal/api_response'
|
|
9
|
+
require_relative './odeon_uk/internal/guest_token'
|
|
10
|
+
require_relative './odeon_uk/internal/title_sanitizer'
|
|
11
|
+
|
|
12
|
+
require_relative './odeon_uk/internal/parser/api/showtime'
|
|
13
|
+
|
|
14
|
+
require_relative './odeon_uk/cinema'
|
|
15
|
+
require_relative './odeon_uk/performance'
|
|
16
|
+
|
|
17
|
+
# odeon_uk gem
|
|
18
|
+
module OdeonUk
|
|
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
|