cineworld_uk 3.1.0 → 4.1.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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.agents/setup +51 -0
  3. data/.github/workflows/test.yml +43 -0
  4. data/.gitignore +1 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +21 -0
  7. data/README.md +136 -16
  8. data/Rakefile +5 -37
  9. data/cineworld_uk.gemspec +7 -4
  10. data/lib/cineworld_uk/cinema.rb +7 -4
  11. data/lib/cineworld_uk/internal/api_response.rb +99 -56
  12. data/lib/cineworld_uk/internal/guest_token.rb +73 -0
  13. data/lib/cineworld_uk/internal/parser/api/cinema_address.rb +24 -24
  14. data/lib/cineworld_uk/internal/parser/api/film.rb +10 -8
  15. data/lib/cineworld_uk/internal/parser/api/performance.rb +6 -11
  16. data/lib/cineworld_uk/performance.rb +42 -23
  17. data/lib/cineworld_uk/version.rb +2 -2
  18. data/lib/cineworld_uk.rb +12 -2
  19. data/tasks/package.rake +84 -0
  20. data/test/fixtures/api/cinema-list.json +1 -1
  21. data/test/fixtures/api/dates-14.json +1 -0
  22. data/test/fixtures/api/performances-tomorrow-14.json +1 -0
  23. data/test/fixtures/api/vista-showtimes-brighton.json +1 -0
  24. data/test/lib/cineworld_uk/cinema_test.rb +47 -47
  25. data/test/lib/cineworld_uk/internal/api_response_test.rb +177 -58
  26. data/test/lib/cineworld_uk/internal/parser/api/cinema_address_test.rb +14 -14
  27. data/test/lib/cineworld_uk/internal/parser/api/film_test.rb +43 -55
  28. data/test/lib/cineworld_uk/internal/parser/api/performance_test.rb +11 -12
  29. data/test/lib/cineworld_uk/internal/title_sanitizer_test.rb +18 -18
  30. data/test/lib/cineworld_uk/performance_test.rb +31 -32
  31. data/test/lib/cineworld_uk/version_test.rb +1 -1
  32. data/test/live/integration_test.rb +9 -9
  33. data/test/package_test.rb +49 -0
  34. data/test/support/fixture_reader.rb +2 -14
  35. data/test/test_helper.rb +3 -2
  36. metadata +65 -36
  37. data/.travis.yml +0 -10
  38. data/lib/cineworld_uk/internal/parser/api/film_lookup.rb +0 -38
  39. data/lib/cineworld_uk/internal/parser/api/performances_by_day.rb +0 -54
  40. data/test/fixtures/api/cinema-detail-10.json +0 -1
  41. data/test/fixtures/api/cinema-detail-21.json +0 -1
  42. data/test/fixtures/api/cinema-detail-3.json +0 -1
  43. data/test/fixtures/api/cinema-detail-96.json +0 -1
  44. data/test/fixtures/api/dates-3.json +0 -1
  45. data/test/fixtures/api/film-list-comingsoon.json +0 -1
  46. data/test/fixtures/api/film-list.json +0 -1
  47. data/test/fixtures/api/performances-tomorrow-3.json +0 -1
  48. data/test/lib/cineworld_uk/internal/parser/api/film_lookup_test.rb +0 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 140adb25b3739c911cd583ddf7a292476389a550
4
- data.tar.gz: f97eed368e77a6f857f619152c9c9d13bab72f05
2
+ SHA256:
3
+ metadata.gz: edfbb0e8f7d6238e878f4018a643befca62b662fffc47cad72367c2e404e9ee7
4
+ data.tar.gz: a0d59a9aa4d3227794fda2f78c00de9e59c62321b3cba3a50dbbd8611856cc82
5
5
  SHA512:
6
- metadata.gz: 981368088d34508277ede0f077f0de5bf736b61c5d16425d01abe0624947c5900e034e7b7c20f43f663c4d79f88c8cf9ea33f2aaefe4007b63c1c981fb6e8714
7
- data.tar.gz: 424e8ec87306e6a6665192c8a7a3b0f10f871af422c2e2cae44befc940da62a8f5fa282015e5c5eb1acd073f51bfa6dd1990ce56aa524da6123796abe39eabf9
6
+ metadata.gz: aefe630b3ad344c2fbced398fc49c5fb9686387331f2cdea072f6dc96ef0893db42af90a6d277117cc7cd1b6ab4505b2fff3e64e859f79896503a71462934a89
7
+ data.tar.gz: cd3ad38fb9b9404504e5379ae03943424d56370b6ce2b0bcda069c9f64fa33a2802ce54dcb567740689408e882bda8e86cba3127193fb510f46fc73b2610c030
data/.agents/setup ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$repo_root"
6
+
7
+ echo 'Checking native gem build prerequisites'
8
+ missing=()
9
+ for package in ca-certificates curl build-essential pkg-config libyaml-dev libssl-dev zlib1g-dev; do
10
+ if ! dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q 'install ok installed'; then
11
+ missing+=("$package")
12
+ fi
13
+ done
14
+ if ((${#missing[@]})); then
15
+ sudo apt-get update -qq
16
+ sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing[@]}"
17
+ fi
18
+
19
+ export PATH="$HOME/.local/bin:$PATH"
20
+ if ! command -v mise >/dev/null; then
21
+ echo 'Installing mise'
22
+ curl --fail --silent --show-error --location https://mise.run | sh
23
+ fi
24
+
25
+ ruby_version="$(tr -d '[:space:]' < .ruby-version)"
26
+ echo "Installing Ruby $ruby_version"
27
+ MISE_RUBY_COMPILE=false mise install "ruby@$ruby_version"
28
+
29
+ # Make the pinned Ruby available to later Amp commands in login shells.
30
+ if [[ ! -f "$HOME/.bash_profile" ]]; then
31
+ printf '[ ! -f "$HOME/.profile" ] || . "$HOME/.profile"\n' > "$HOME/.bash_profile"
32
+ fi
33
+ marker="# Cineworld orb Ruby environment: $repo_root"
34
+ if ! grep -Fqx "$marker" "$HOME/.bash_profile"; then
35
+ cat >> "$HOME/.bash_profile" <<EOF
36
+
37
+ $marker
38
+ case "\$PWD" in
39
+ "$repo_root"|"$repo_root"/*)
40
+ export PATH="\$HOME/.local/share/mise/shims:\$HOME/.local/bin:\$PATH"
41
+ export MISE_IDIOMATIC_VERSION_FILE_ENABLE_TOOLS=ruby
42
+ ;;
43
+ esac
44
+ EOF
45
+ fi
46
+
47
+ echo 'Installing gem dependencies (no Cineworld token required)'
48
+ mise exec "ruby@$ruby_version" -- bundle config set --local path "$HOME/.cache/cineworld_uk/bundle"
49
+ mise exec "ruby@$ruby_version" -- bundle check || mise exec "ruby@$ruby_version" -- bundle install --jobs 4 --retry 3
50
+ mise reshim
51
+ mise exec "ruby@$ruby_version" -- ruby --version
@@ -0,0 +1,43 @@
1
+ name: Tests
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ ruby-version: ['3.3', '3.4', '4.0']
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: ${{ matrix.ruby-version }}
16
+ bundler-cache: true
17
+ - run: bundle exec rake test
18
+
19
+ package:
20
+ strategy:
21
+ matrix:
22
+ include:
23
+ - runner: ubuntu-latest
24
+ platform: x86_64-linux-gnu
25
+ - runner: ubuntu-24.04-arm
26
+ platform: aarch64-linux-gnu
27
+ - runner: macos-15
28
+ platform: arm64-darwin
29
+ - runner: macos-15-intel
30
+ platform: x86_64-darwin
31
+ runs-on: ${{ matrix.runner }}
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: ruby/setup-ruby@v1
35
+ with:
36
+ ruby-version: '3.4'
37
+ bundler-cache: true
38
+ - run: bundle exec rake 'package:native[${{ matrix.platform }}]'
39
+ - run: CINEWORLD_PACKAGE="$(echo pkg/*.gem)" bundle exec ruby test/package_test.rb
40
+ - uses: actions/upload-artifact@v4
41
+ with:
42
+ name: gem-${{ matrix.platform }}
43
+ path: pkg/*.gem
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .amp/portals/*
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
  All notable changes to this project will be documented in this file.
3
3
  This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
+ ## 4.1.0 - 2026-10-15
6
+
7
+ ### Added
8
+ - Explicit guest token acquisition and renewal with `CineworldUk.acquire_token!`, using the standalone `curl-impersonate` CLI without Python or a browser.
9
+ - Platform-specific gems bundle curl-impersonate 2.2.3 for x86-64/ARM64 glibc Linux and macOS, with pinned archive checksums, upstream notices, and native package CI checks. No separate executable installation is needed on these platforms.
10
+ - Optional executable path for token acquisition. Existing caller-supplied tokens continue to work without the CLI.
11
+ - Expiry checks and authentication errors for unavailable commands, blocked pages, and missing or invalid page tokens; failed acquisition preserves the previous token.
12
+
13
+ ## 4.0.0 - 2026-10-15
14
+
15
+ ### Changed
16
+ - Retrieve cinemas and performances from the Vista API instead of the retired iOS API.
17
+ - Require a caller-supplied guest JWT through `CineworldUk.token`; token acquisition and renewal remain the caller's responsibility.
18
+ - `Performance.at` accepts inclusive `from:` and `to:` business dates, defaulting to today in London and the following 13 days rather than discovering all screening dates.
19
+ - Use one request per cinema per requested day, reusing related film, cinema and attribute data. Share cached cinema data with address lookups.
20
+ - Convert advertised session times to UTC using Vista's explicit offsets and preserve session-specific formats.
21
+
22
+ ### Added
23
+ - `CineworldUk::AuthenticationError` for missing tokens and HTTP 401 responses.
24
+ - Orb setup for the pinned Ruby version and gem dependencies.
25
+
5
26
  ## 3.1.0 - 2016-09-11
6
27
 
7
28
  ### Fixed
data/README.md CHANGED
@@ -1,12 +1,9 @@
1
1
  # CineworldUk
2
2
 
3
- A simple gem to parse the [Cineworld UK](http://cineworld.co.uk) cinema times (using their iOS API) and spit out useful formatted info.
3
+ A gem to retrieve [Cineworld UK](https://www.cineworld.co.uk) cinemas and showtimes using the Vista API.
4
4
 
5
5
  [![Gem Version](https://badge.fury.io/rb/cineworld_uk.svg)](https://badge.fury.io/rb/cineworld_uk)
6
- [![Code Climate](https://codeclimate.com/github/andycroll/cineworld_uk/badges/gpa.svg)](https://codeclimate.com/github/andycroll/cineworld_uk)
7
- [![Test Coverage](https://codeclimate.com/github/andycroll/cineworld_uk/badges/coverage.svg)](https://codeclimate.com/github/andycroll/cineworld_uk/coverage)
8
- [![Build Status](https://travis-ci.org/andycroll/cineworld_uk.svg?branch=master)](https://travis-ci.org/andycroll/cineworld_uk)
9
- [![Inline docs](http://inch-ci.org/github/andycroll/cineworld_uk.svg?branch=master)](http://inch-ci.org/github/andycroll/cineworld_uk)
6
+ [![Tests](https://github.com/andycroll/cineworld_uk/actions/workflows/.travis.yml/badge.svg)](https://github.com/andycroll/cineworld_uk/actions)
10
7
 
11
8
  ## Installation
12
9
 
@@ -28,13 +25,94 @@ The gem conforms to the API set down in the `cinebase` gem, [andycroll/cinebase]
28
25
 
29
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 Opera House etc).
30
27
 
28
+ ### Authentication
29
+
30
+ Acquire a guest JWT explicitly before retrieving data:
31
+
32
+ ```ruby
33
+ require 'cineworld_uk'
34
+
35
+ CineworldUk.acquire_token!
36
+ CineworldUk::Cinema.all
37
+ ```
38
+
39
+ This shells out to **`curl-impersonate`**, not ordinary system `curl`, using its
40
+ Chrome 150 TLS/HTTP profile. No Python, Chrome, JavaScript execution or browser
41
+ cookies are needed. Platform-specific gem packages include the pinned
42
+ `curl-impersonate` 2.2.3 executable and its upstream licence notices. RubyGems
43
+ and Bundler automatically select the matching package for:
44
+
45
+ - glibc Linux: x86-64 and ARM64 (`x86_64-linux-gnu`, `aarch64-linux-gnu`)
46
+ - macOS: Intel and Apple Silicon (`x86_64-darwin`, `arm64-darwin`)
47
+
48
+ The executable runs directly from the installed gem. There are no separate
49
+ installation commands, PATH changes, or runtime downloads on these platforms.
50
+
51
+ When installing from Git, forcing the source (`ruby`) gem, or using another
52
+ platform (including Alpine/musl and Windows), install a compatible
53
+ [curl-impersonate release](https://github.com/lexiforest/curl-impersonate/releases/tag/v2.2.3)
54
+ with `--impersonate chrome150` support yourself. For an x86-64 Linux checkout:
55
+
56
+ ```sh
57
+ mkdir -p "$HOME/.local/bin"
58
+ curl -fL https://github.com/lexiforest/curl-impersonate/releases/download/v2.2.3/curl-impersonate-v2.2.3.x86_64-linux-gnu.tar.gz \
59
+ | tar -xz -C "$HOME/.local/bin" curl-impersonate
60
+ export PATH="$HOME/.local/bin:$PATH"
61
+ ```
62
+
63
+ Other platforms have separate assets on the release page. You can also specify
64
+ the executable path:
65
+
66
+ ```ruby
67
+ CineworldUk.acquire_token!(curl: '/opt/curl-impersonate/curl-impersonate')
68
+ ```
69
+
70
+ The method returns the token and sets `CineworldUk.token` in memory. It reads
71
+ `__NEXT_DATA__.props.pageProps.environment.gasToken` from the ticketing site's
72
+ root page, even when the page displays a 404. It checks the declared expiry,
73
+ but decoding the JWT does not verify its signature. A blocked page, expired or
74
+ missing token, missing executable, or failed command raises
75
+ `CineworldUk::AuthenticationError` and leaves any previous token unchanged.
76
+ Requests time out after 30 seconds. Certificate verification remains enabled;
77
+ redirects are not followed, and subprocess arguments are passed without a shell.
78
+ Cloudflare may still block requests on other networks or after website changes.
79
+
80
+ Alternatively, supply your own current guest JWT without the `Bearer ` prefix;
81
+ this requires no external executable:
82
+
83
+ ```ruby
84
+ CineworldUk.token = ENV.fetch('CINEWORLD_TOKEN')
85
+ ```
86
+
87
+ The gem sends `Authorization: Bearer ...` and `Connect-Region-Code: GB` to
88
+ `digital-api.cineworld.co.uk`. Normal API calls never acquire or renew a token
89
+ automatically.
90
+
91
+ Missing tokens and HTTP 401 responses raise `CineworldUk::AuthenticationError`.
92
+ Call `CineworldUk.acquire_token!` again, or set `CineworldUk.token` with a fresh
93
+ token, before retrying. Tokens are never persisted by the gem. Other HTTP
94
+ failures raise an error rather than returning an empty schedule. Redirects are
95
+ not followed, so credentials are not forwarded to another host.
96
+
97
+ There is no verified fixed token lifetime. Its declared lifetime is `exp - iat`
98
+ seconds. The website reloads to renew after 90% of that interval. To inspect
99
+ your token locally without printing it (decoding does not verify its signature):
100
+
101
+ ```ruby
102
+ require 'base64'
103
+ payload = CineworldUk.token.split('.').fetch(1)
104
+ claims = JSON.parse(Base64.urlsafe_decode64(payload))
105
+ puts "Lifetime: #{claims.fetch('exp') - claims.fetch('iat')} seconds"
106
+ puts "Expires: #{Time.at(claims.fetch('exp')).utc}"
107
+ ```
108
+
31
109
  ### Cinema
32
110
 
33
111
  ``` ruby
34
112
  CineworldUk::Cinema.all
35
113
  #=> [<CineworldUK::Cinema ...>, <CineworldUK::Cinema ...>, ...]
36
114
 
37
- cinema = CineworldUk::Cinema.new(3)
115
+ cinema = CineworldUk::Cinema.new(14)
38
116
  #=> <CineworldUK::Cinema ...>
39
117
 
40
118
  cinema.adr
@@ -42,7 +120,7 @@ cinema.adr
42
120
  street_address: 'Brighton Marina Village',
43
121
  extended_address: nil,
44
122
  locality: 'Brighton',
45
- region: nil,
123
+ region: 'East Sussex',
46
124
  postal_code: 'BN2 5UF',
47
125
  country: 'United Kingdom'
48
126
  }
@@ -54,7 +132,7 @@ cinema.full_name
54
132
  #=> 'Cineworld Brighton'
55
133
 
56
134
  cinema.id
57
- #=> '3'
135
+ #=> 14
58
136
 
59
137
  cinema.postal_code
60
138
  #=> 'BN2 5UF'
@@ -62,38 +140,80 @@ cinema.postal_code
62
140
 
63
141
  ### Performances
64
142
 
143
+ `at` accepts a numeric Vista cinema ID (`14`, `'14'` or `'014'` for Brighton),
144
+ not a Boxoffice ID such as `X079B`. Dates are inclusive **business dates**.
145
+ Without dates, it fetches today in London and the following 13 days; it does not
146
+ discover every future screening date.
147
+
148
+ ```ruby
149
+ day = Date.new(2026, 10, 14)
150
+ performances = CineworldUk::Performance.at(14, from: day, to: day)
151
+ ```
152
+
153
+ This makes **one HTTP request per cinema per requested day**, including empty
154
+ days. Cinema names, film titles and attributes come from the same response—no
155
+ extra cinema or film lookups are needed. `Cinema.all` separately fetches and
156
+ caches the cinema list in memory, shared by name and address lookups. Performance
157
+ responses are not cached across calls, so reuse the returned array when possible.
158
+
159
+ Advertised session times (`startsAt`), not feature start times (`filmStartsAt`),
160
+ are converted to UTC using the offsets supplied by Vista. Recognized session
161
+ attributes populate the existing dimension and variant fields; unknown attributes
162
+ are ignored. No seat-availability or booking operations are performed.
163
+
65
164
  ``` ruby
66
- CineworldUk::Performance.at(3)
165
+ CineworldUk::Performance.at(14)
67
166
  #=> [<CineworldUK::Performance ...>, <CineworldUK::Performance ...>, ...]
68
167
 
69
- performance = CineworldUk::Performance.at(3).first
168
+ performance = performances.first
70
169
  #=> <CineworldUK::Performance ...>
71
170
 
72
171
  performance.film_name
73
- #=> 'Star Wars: The Force Awakens'
172
+ #=> 'Other Mommy'
74
173
 
75
174
  performance.dimension
76
175
  #=> '2d'
77
176
 
78
177
  performance.variant
79
- #=> ['imax', 'kids']
178
+ #=> ['audio_described']
80
179
 
81
180
  performance.starting_at
82
- #=> 2016-02-04 13:00:20 UTC
181
+ #=> 2026-10-14 14:40:00 UTC
83
182
 
84
183
  performance.showing_on
85
- #=> #<Date: 2016-02-04 ((2457423j,0s,0n),+0s,2299161j)>
184
+ #=> Date.new(2026, 10, 14)
86
185
 
87
186
  performance.booking_url
88
- #=> "http://www.cineworld.co.uk/booking?performance=..."
187
+ #=> "https://web.cineworld.co.uk/order/showtimes/014-165507/seats"
89
188
 
90
189
  performance.cinema_name
91
190
  #=> 'Brighton'
92
191
 
93
192
  performance.cinema_id
94
- #=> 3
193
+ #=> 14
95
194
  ```
96
195
 
196
+ ## Building release packages
197
+
198
+ ```sh
199
+ bundle exec rake build # Portable source gem; external curl-impersonate required
200
+ bundle exec rake package:all # Four platform gems with bundled executables
201
+ ```
202
+
203
+ To build and check just one platform on a matching host:
204
+
205
+ ```sh
206
+ bundle exec rake 'package:native[x86_64-linux-gnu]'
207
+ CINEWORLD_PACKAGE=pkg/cineworld_uk-4.1.0-x86_64-linux-gnu.gem bundle exec ruby test/package_test.rb
208
+ ```
209
+
210
+ Native packaging downloads pinned upstream archives at **build time**, verifies
211
+ their SHA-256 digests, and includes the executable, licence files and source
212
+ provenance. Archives and built gems stay in ignored `pkg/`, not Git. CI builds
213
+ and installation-tests each platform on its corresponding runner and saves the
214
+ gems as workflow artifacts; it does not publish them. Publish the source and
215
+ platform gems together for automatic platform selection to work for users.
216
+
97
217
  ## Contributing
98
218
 
99
219
  1. Fork it
data/Rakefile CHANGED
@@ -2,6 +2,7 @@
2
2
  require "bundler/gem_tasks"
3
3
 
4
4
  require 'rake/testtask'
5
+ load File.expand_path('tasks/package.rake', __dir__)
5
6
 
6
7
  Rake::TestTask.new do |t|
7
8
  t.libs << 'lib/cineworld_uk'
@@ -14,7 +15,7 @@ Rake::TestTask.new do |t|
14
15
  end
15
16
 
16
17
  Rake::TestTask.new do |t|
17
- t.libs << 'lib/picturehouse_uk'
18
+ t.libs << 'lib/cineworld_uk'
18
19
  t.name = :live
19
20
  t.test_files = FileList[
20
21
  'test/live/*_test.rb'
@@ -35,6 +36,7 @@ end
35
36
  desc 'Recreate test fixtures'
36
37
  task :fixtures do
37
38
  require 'cineworld_uk'
39
+ CineworldUk.token = ENV.fetch("CINEWORLD_TOKEN")
38
40
 
39
41
  def js_fixture(name)
40
42
  File.expand_path("../test/fixtures/#{name}.json", __FILE__)
@@ -47,45 +49,11 @@ task :fixtures do
47
49
  file.write api.cinema_list
48
50
  end
49
51
 
50
- File.open(js_fixture('api/cinema-detail-3'), 'w') do |file|
51
- puts '* API Cinema Detail Brighton'
52
- file.write api.cinema_detail(3)
53
- end
54
-
55
- File.open(js_fixture('api/cinema-detail-96'), 'w') do |file|
56
- puts '* API Cinema Detail Birmingham NEC'
57
- file.write api.cinema_detail(96)
58
- end
59
-
60
- File.open(js_fixture('api/cinema-detail-21'), 'w') do |file|
61
- puts '* API Cinema Detail Edinburgh'
62
- file.write api.cinema_detail(21)
63
- end
64
-
65
- File.open(js_fixture('api/cinema-detail-10'), 'w') do |file|
66
- puts '* API Cinema Detail Chelsea'
67
- file.write api.cinema_detail(10)
68
- end
69
-
70
- File.open(js_fixture('api/film-list'), 'w') do |file|
71
- puts '* API Film List'
72
- file.write api.film_list
73
- end
74
-
75
- File.open(js_fixture('api/film-list-comingsoon'), 'w') do |file|
76
- puts '* API Film List Coming Soon'
77
- file.write api.film_list_comingsoon
78
- end
79
-
80
- File.open(js_fixture('api/performances-tomorrow-3'), 'w') do |file|
52
+ File.open(js_fixture('api/performances-tomorrow-14'), 'w') do |file|
81
53
  puts '* API Performances in Brighton Tomorrow'
82
- file.write api.performances(3, Date.today + 1)
54
+ file.write api.performances(14, Date.today + 1)
83
55
  end
84
56
 
85
- File.open(js_fixture('api/dates-3'), 'w') do |file|
86
- puts '* API Dates Brighton Tomorrow'
87
- file.write api.dates(3)
88
- end
89
57
  end
90
58
 
91
59
  task :default => :test
data/cineworld_uk.gemspec CHANGED
@@ -12,16 +12,19 @@ Gem::Specification.new do |spec|
12
12
  spec.homepage = 'https://github.com/andycroll/cineworld_uk'
13
13
  spec.licenses = %w(AGPL MIT)
14
14
 
15
- spec.files = `git ls-files`.split($RS)
15
+ spec.files = `git ls-files`.split("\n")
16
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ['lib']
19
19
 
20
- spec.add_development_dependency 'bundler', '~> 1.3'
21
- spec.add_development_dependency 'codeclimate-test-reporter'
20
+ spec.add_development_dependency 'bundler', '>= 2.0'
21
+ spec.add_development_dependency 'minitest-mock'
22
22
  spec.add_development_dependency 'minitest-reporters'
23
23
  spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'simplecov', '< 1'
24
25
  spec.add_development_dependency 'webmock'
25
26
 
26
- spec.add_runtime_dependency 'cinebase', '~> 3.0.0'
27
+ spec.add_runtime_dependency 'base64', '~> 0.3'
28
+ spec.add_runtime_dependency 'cinebase', '~> 5.0'
29
+ spec.add_runtime_dependency 'nokogiri', '>= 1.11'
27
30
  end
@@ -25,8 +25,9 @@ module CineworldUk
25
25
  # @return [Hash<Integer => String>]
26
26
  def self.id_names_hash
27
27
  @id_names_hash ||= cinema_list_json.each_with_object({}) do |hash, result|
28
- result[hash['id']] =
29
- hash['name'].gsub('London - ', '').gsub(' - ', ': ')
28
+ name = hash['displayName']
29
+ id_key = hash['id'].to_i
30
+ result[id_key] = name.gsub('London - ', '').gsub(' - ', ': ') if name
30
31
  end
31
32
  end
32
33
 
@@ -49,7 +50,9 @@ module CineworldUk
49
50
  # }
50
51
  # @note Uses the standard method naming as at http://microformats.org/wiki/adr
51
52
  def adr
52
- CineworldUk::Internal::Parser::Api::CinemaAddress.new(@id).to_hash
53
+ CineworldUk::Internal::Parser::Api::CinemaAddress.new(
54
+ @id, self.class.send(:cinema_list_json)
55
+ ).to_hash
53
56
  end
54
57
 
55
58
  # Brand of the cinema
@@ -158,7 +161,7 @@ module CineworldUk
158
161
  # @api private
159
162
  def self.cinema_list_json
160
163
  @cinema_list_json ||=
161
- JSON.parse(api.cinema_list)['cinemas']
164
+ JSON.parse(api.cinema_list)['body']['cinemas']
162
165
  end
163
166
  private_class_method :cinema_list_json
164
167
  end
@@ -1,72 +1,115 @@
1
- require 'net/http'
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "date"
6
+ require "time"
2
7
 
3
8
  module CineworldUk
4
- # @api private
5
9
  module Internal
6
- # @api private
10
+ # Adapts Vista responses to the parsers' cinema/film/event vocabulary.
7
11
  class ApiResponse
8
- # Basic cinema list of ids/names
9
- # @return [String] JSON encoded
10
- def cinema_list
11
- response('cinema/list', full: true)
12
- end
13
-
14
- # List of dates on which there are screening for this cinema id
15
- # @param [Fixnum] id the id of the cinema
16
- # @return [String] JSON encoded
17
- def dates(id)
18
- response('dates', cinema: id)
19
- end
12
+ BASE_URL = "https://digital-api.cineworld.co.uk/ocapi/v1/"
13
+ ATTRIBUTES = {
14
+ "3D" => "3d", "IMAX" => "imax", "AD" => "audio-described",
15
+ "Subtitled" => "subbed", "Subtitles" => "subbed",
16
+ "Autism Friendly" => "autism-friendly",
17
+ "Movies for Juniors" => "junior",
18
+ "Unlimited Screening" => "unlimited-screening", "Q&A" => "qa"
19
+ }.freeze
20
20
 
21
- # Detail about a specific cinema
22
- # @param [Fixnum] id the id of the cinema
23
- # @return [String] JSON encoded
24
- def cinema_detail(id)
25
- response('cinema/detail', cinema: id)
26
- end
27
-
28
- # List of all currently showing films
29
- # @return [String] JSON encoded
30
- def film_list
31
- response('film/list', full: true)
32
- end
33
-
34
- # List of all upcoming films
35
- # @return [String] JSON encoded
36
- def film_list_comingsoon
37
- response('film/list/comingsoon', full: true)
21
+ def cinema_list
22
+ cinemas = fetch("sites").fetch("sites").map do |site|
23
+ address = site.fetch("contactDetails", {}).fetch("addressV2", {})
24
+ {
25
+ "id" => site.fetch("id"),
26
+ "displayName" => site.fetch("name").fetch("text"),
27
+ "addressInfo" => {
28
+ "address1" => address.dig("line1", "text"),
29
+ "city" => address.dig("line2", "text"),
30
+ "state" => address.dig("line3", "text"),
31
+ "postalCode" => address.dig("line4", "text")
32
+ }
33
+ }
34
+ end
35
+ JSON.generate("body" => { "cinemas" => cinemas })
38
36
  end
39
37
 
40
- # All screenings from a specific cinema on a specific date
41
- # @param [Fixnum] cinema_id the id of the cinema
42
- # @param [Date] date a single date in the future
43
- # @return [String] JSON encoded
44
38
  def performances(cinema_id, date)
45
- response('performances', cinema: cinema_id,
46
- date: date.strftime('%Y%m%d'))
39
+ site_id = cinema_id.to_s.rjust(3, "0")
40
+ response = fetch("showtimes/by-business-date/#{date.iso8601}",
41
+ "siteIds" => site_id)
42
+ related = response.fetch("relatedData")
43
+ films = related.fetch("films").to_h { |film| [film.fetch("id"), film] }
44
+ attributes = related.fetch("attributes").to_h do |attr|
45
+ [attr.fetch("id"), attr]
46
+ end
47
+ normalized_films = []
48
+ events = response.fetch("showtimes").map do |showtime|
49
+ film = films.fetch(showtime.fetch("filmId"))
50
+ attrs = showtime.fetch("attributeIds", []).filter_map do |id|
51
+ attribute = attributes.fetch(id)
52
+ ATTRIBUTES[attribute.dig("shortName", "text")] ||
53
+ ATTRIBUTES[attribute.dig("name", "text")]
54
+ end
55
+ attrs << "3d" if showtime["requires3dGlasses"]
56
+ # Formats belong to a session, not to the film as a whole.
57
+ id = showtime.fetch("id")
58
+ normalized_films << {
59
+ "id" => id,
60
+ "name" => film.fetch("title").fetch("text"),
61
+ "attributeIds" => attrs
62
+ }
63
+ starts_at = showtime.fetch("schedule").fetch("startsAt")
64
+ {
65
+ "filmId" => id, "attributeIds" => attrs,
66
+ "eventDateTime" => Time.iso8601(starts_at).utc.iso8601,
67
+ "bookingLink" =>
68
+ "https://web.cineworld.co.uk/order/showtimes/#{id}/seats"
69
+ }
70
+ end
71
+ site = related.fetch("sites").detect do |item|
72
+ item.fetch("id") == site_id
73
+ end
74
+ JSON.generate(
75
+ "body" => {
76
+ "films" => normalized_films, "events" => events,
77
+ "cinemaName" => site&.dig("name", "text")
78
+ }
79
+ )
47
80
  end
48
81
 
49
82
  private
50
83
 
51
- # @api private
52
- # mixin the default hash
53
- DEFAULTS = { key: 'ios', territory: 'GB' }
54
-
55
- def fetch(uri, limit = 10)
56
- fail ArgumentError, 'too many HTTP redirects' if limit == 0
57
-
58
- case response = Net::HTTP.get_response(URI(uri))
59
- when Net::HTTPSuccess then response.body
60
- when Net::HTTPRedirection then fetch(response['location'], limit - 1)
61
- else response.code
84
+ def fetch(path, query = {})
85
+ token = CineworldUk.token
86
+ if !token.is_a?(String) || token.strip.empty?
87
+ raise AuthenticationError,
88
+ "Set CineworldUk.token to a current guest JWT " \
89
+ "before fetching data"
90
+ end
91
+ uri = URI(BASE_URL + path)
92
+ uri.query = URI.encode_www_form(query) unless query.empty?
93
+ request = Net::HTTP::Get.new(uri)
94
+ request["Authorization"] = "Bearer #{token}"
95
+ request["Connect-Region-Code"] = "GB"
96
+ request["Accept"] = "application/json"
97
+ response = Net::HTTP.start(
98
+ uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30
99
+ ) do |http|
100
+ http.request(request)
101
+ end
102
+ case response
103
+ when Net::HTTPSuccess
104
+ JSON.parse(response.body)
105
+ when Net::HTTPUnauthorized
106
+ raise AuthenticationError,
107
+ "Vista rejected the guest token; " \
108
+ "supply a fresh token with CineworldUk.token="
109
+ else
110
+ # Omit response bodies and never redirect credentials to another host.
111
+ raise "Vista HTTP error #{response.code}"
62
112
  end
63
- end
64
-
65
- def response(path, hash)
66
- uri = URI::HTTPS.build(host: 'www.cineworld.co.uk',
67
- path: "/api/#{path}",
68
- query: URI.encode_www_form(hash.merge(DEFAULTS)))
69
- fetch(uri)
70
113
  end
71
114
  end
72
115
  end