cineworld_uk 3.0.4 → 4.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.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/.agents/setup +51 -0
  3. data/.github/workflows/test.yml +17 -0
  4. data/.gitignore +1 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +21 -0
  7. data/README.md +67 -16
  8. data/Rakefile +12 -36
  9. data/cineworld_uk.gemspec +6 -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/parser/api/cinema_address.rb +24 -24
  13. data/lib/cineworld_uk/internal/parser/api/film.rb +10 -8
  14. data/lib/cineworld_uk/internal/parser/api/performance.rb +6 -11
  15. data/lib/cineworld_uk/performance.rb +42 -23
  16. data/lib/cineworld_uk/version.rb +2 -2
  17. data/lib/cineworld_uk.rb +6 -2
  18. data/test/fixtures/api/cinema-list.json +1 -1
  19. data/test/fixtures/api/dates-14.json +1 -0
  20. data/test/fixtures/api/performances-tomorrow-14.json +1 -0
  21. data/test/fixtures/api/vista-showtimes-brighton.json +1 -0
  22. data/test/lib/cineworld_uk/cinema_test.rb +47 -47
  23. data/test/lib/cineworld_uk/internal/api_response_test.rb +111 -68
  24. data/test/lib/cineworld_uk/internal/parser/api/cinema_address_test.rb +14 -14
  25. data/test/lib/cineworld_uk/internal/parser/api/film_test.rb +43 -55
  26. data/test/lib/cineworld_uk/internal/parser/api/performance_test.rb +11 -12
  27. data/test/lib/cineworld_uk/internal/title_sanitizer_test.rb +18 -18
  28. data/test/lib/cineworld_uk/performance_test.rb +31 -32
  29. data/test/lib/cineworld_uk/version_test.rb +1 -1
  30. data/test/live/integration_test.rb +51 -0
  31. data/test/support/fixture_reader.rb +2 -14
  32. data/test/test_helper.rb +3 -2
  33. metadata +49 -36
  34. data/.travis.yml +0 -10
  35. data/lib/cineworld_uk/internal/parser/api/film_lookup.rb +0 -38
  36. data/lib/cineworld_uk/internal/parser/api/performances_by_day.rb +0 -54
  37. data/test/fixtures/api/cinema-detail-10.json +0 -1
  38. data/test/fixtures/api/cinema-detail-21.json +0 -1
  39. data/test/fixtures/api/cinema-detail-3.json +0 -1
  40. data/test/fixtures/api/cinema-detail-96.json +0 -1
  41. data/test/fixtures/api/dates-3.json +0 -1
  42. data/test/fixtures/api/film-list-comingsoon.json +0 -1
  43. data/test/fixtures/api/film-list.json +0 -1
  44. data/test/fixtures/api/performances-tomorrow-3.json +0 -1
  45. 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: 05bf58c0a5b7017cf11d3ccc42db151628f61549
4
- data.tar.gz: 2ef345fb4bbd547ae2dcbb3f8ea348a86b18b8a0
2
+ SHA256:
3
+ metadata.gz: 5284f7c7c8b846edbdd724cb3c105b033da4f313eed083d6b691ae6b8714d3b8
4
+ data.tar.gz: 1568acd04ed0346ba388509903a17487726c088ff2d280329cfb0fe8f6023eed
5
5
  SHA512:
6
- metadata.gz: 9ca64cf5628c0f02d6890a736d36c8b342e06963ad8fa4fa69a7a5639668b51fb92e0327ecabb0f33c5929093c35c112988d8bf42287e0540f28310a5979e3e4
7
- data.tar.gz: ae4e5ebf966d324d971c3be8b28e865ea709df88c0f395f046cee08ba66b8410424415be6bd2f655a7715f98ae7986a86bb5bf4ca50011224ee817b6a0898a6a
6
+ metadata.gz: faff1f58b5e101ce8185a96f791a27088ed31f7c78d3eebf3f9b332b6931848604726b7e5b3d16f3bb51c1878c4693cb993863cf48dcbbe743436208e5c3186b
7
+ data.tar.gz: 2ab7741bafc40d66d104390340fa2e28a1a5b0a6d166f85b3526f8eb576d664b6860c5f03032fc3d71a2c21bb3ece25804d5258aeba3a7f74601027610c161d8
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,17 @@
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
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.0.0 - 2026-10-15
6
+
7
+ ### Changed
8
+ - Retrieve cinemas and performances from the Vista API instead of the retired iOS API.
9
+ - Require a caller-supplied guest JWT through `CineworldUk.token`; token acquisition and renewal remain the caller's responsibility.
10
+ - `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.
11
+ - Use one request per cinema per requested day, reusing related film, cinema and attribute data. Share cached cinema data with address lookups.
12
+ - Convert advertised session times to UTC using Vista's explicit offsets and preserve session-specific formats.
13
+
14
+ ### Added
15
+ - `CineworldUk::AuthenticationError` for missing tokens and HTTP 401 responses.
16
+ - Orb setup for the pinned Ruby version and gem dependencies.
17
+
18
+ ## 3.1.0 - 2016-09-11
19
+
20
+ ### Fixed
21
+ - Use of www2 domain, set to HTTPS and www
22
+
23
+ ### Added
24
+ - Tests against live server, using `rake live`
25
+
5
26
  ## 3.0.4 - 2016-04-26
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,46 @@ 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
+ Configure a current guest JWT before retrieving data (without the `Bearer ` prefix):
31
+
32
+ ```ruby
33
+ require 'cineworld_uk'
34
+
35
+ CineworldUk.token = ENV.fetch('CINEWORLD_TOKEN')
36
+ ```
37
+
38
+ The gem sends `Authorization: Bearer ...` and `Connect-Region-Code: GB` to
39
+ `digital-api.cineworld.co.uk`. It does not acquire or refresh tokens, copy browser
40
+ cookies, or bypass Cloudflare. The ticketing website supplies its guest token in
41
+ `__NEXT_DATA__.props.pageProps.environment.gasToken`; access to that page may
42
+ require an interactive browser.
43
+
44
+ Missing tokens and HTTP 401 responses raise `CineworldUk::AuthenticationError`.
45
+ Set `CineworldUk.token` again with a fresh token before retrying. Other HTTP
46
+ failures raise an error rather than returning an empty schedule. Redirects are
47
+ not followed, so credentials are not forwarded to another host.
48
+
49
+ There is no verified fixed token lifetime. Its declared lifetime is `exp - iat`
50
+ seconds. The website reloads to renew after 90% of that interval. To inspect
51
+ your token locally without printing it (decoding does not verify its signature):
52
+
53
+ ```ruby
54
+ require 'base64'
55
+ payload = CineworldUk.token.split('.').fetch(1)
56
+ claims = JSON.parse(Base64.urlsafe_decode64(payload))
57
+ puts "Lifetime: #{claims.fetch('exp') - claims.fetch('iat')} seconds"
58
+ puts "Expires: #{Time.at(claims.fetch('exp')).utc}"
59
+ ```
60
+
31
61
  ### Cinema
32
62
 
33
63
  ``` ruby
34
64
  CineworldUk::Cinema.all
35
65
  #=> [<CineworldUK::Cinema ...>, <CineworldUK::Cinema ...>, ...]
36
66
 
37
- cinema = CineworldUk::Cinema.new(3)
67
+ cinema = CineworldUk::Cinema.new(14)
38
68
  #=> <CineworldUK::Cinema ...>
39
69
 
40
70
  cinema.adr
@@ -42,7 +72,7 @@ cinema.adr
42
72
  street_address: 'Brighton Marina Village',
43
73
  extended_address: nil,
44
74
  locality: 'Brighton',
45
- region: nil,
75
+ region: 'East Sussex',
46
76
  postal_code: 'BN2 5UF',
47
77
  country: 'United Kingdom'
48
78
  }
@@ -54,7 +84,7 @@ cinema.full_name
54
84
  #=> 'Cineworld Brighton'
55
85
 
56
86
  cinema.id
57
- #=> '3'
87
+ #=> 14
58
88
 
59
89
  cinema.postal_code
60
90
  #=> 'BN2 5UF'
@@ -62,36 +92,57 @@ cinema.postal_code
62
92
 
63
93
  ### Performances
64
94
 
95
+ `at` accepts a numeric Vista cinema ID (`14`, `'14'` or `'014'` for Brighton),
96
+ not a Boxoffice ID such as `X079B`. Dates are inclusive **business dates**.
97
+ Without dates, it fetches today in London and the following 13 days; it does not
98
+ discover every future screening date.
99
+
100
+ ```ruby
101
+ day = Date.new(2026, 10, 14)
102
+ performances = CineworldUk::Performance.at(14, from: day, to: day)
103
+ ```
104
+
105
+ This makes **one HTTP request per cinema per requested day**, including empty
106
+ days. Cinema names, film titles and attributes come from the same response—no
107
+ extra cinema or film lookups are needed. `Cinema.all` separately fetches and
108
+ caches the cinema list in memory, shared by name and address lookups. Performance
109
+ responses are not cached across calls, so reuse the returned array when possible.
110
+
111
+ Advertised session times (`startsAt`), not feature start times (`filmStartsAt`),
112
+ are converted to UTC using the offsets supplied by Vista. Recognized session
113
+ attributes populate the existing dimension and variant fields; unknown attributes
114
+ are ignored. No seat-availability or booking operations are performed.
115
+
65
116
  ``` ruby
66
- CineworldUk::Performance.at(3)
117
+ CineworldUk::Performance.at(14)
67
118
  #=> [<CineworldUK::Performance ...>, <CineworldUK::Performance ...>, ...]
68
119
 
69
- performance = CineworldUk::Performance.at(3).first
120
+ performance = performances.first
70
121
  #=> <CineworldUK::Performance ...>
71
122
 
72
123
  performance.film_name
73
- #=> 'Star Wars: The Force Awakens'
124
+ #=> 'Other Mommy'
74
125
 
75
126
  performance.dimension
76
127
  #=> '2d'
77
128
 
78
129
  performance.variant
79
- #=> ['imax', 'kids']
130
+ #=> ['audio_described']
80
131
 
81
132
  performance.starting_at
82
- #=> 2016-02-04 13:00:20 UTC
133
+ #=> 2026-10-14 14:40:00 UTC
83
134
 
84
135
  performance.showing_on
85
- #=> #<Date: 2016-02-04 ((2457423j,0s,0n),+0s,2299161j)>
136
+ #=> Date.new(2026, 10, 14)
86
137
 
87
138
  performance.booking_url
88
- #=> "http://www.cineworld.co.uk/booking?performance=..."
139
+ #=> "https://web.cineworld.co.uk/order/showtimes/014-165507/seats"
89
140
 
90
141
  performance.cinema_name
91
142
  #=> 'Brighton'
92
143
 
93
144
  performance.cinema_id
94
- #=> 3
145
+ #=> 14
95
146
  ```
96
147
 
97
148
  ## Contributing
data/Rakefile CHANGED
@@ -13,6 +13,15 @@ Rake::TestTask.new do |t|
13
13
  t.verbose = true
14
14
  end
15
15
 
16
+ Rake::TestTask.new do |t|
17
+ t.libs << 'lib/cineworld_uk'
18
+ t.name = :live
19
+ t.test_files = FileList[
20
+ 'test/live/*_test.rb'
21
+ ]
22
+ t.verbose = true
23
+ end
24
+
16
25
  desc 'console'
17
26
  # http://erniemiller.org/2014/02/05/7-lines-every-gems-rakefile-should-have/
18
27
  task :console do
@@ -26,6 +35,7 @@ end
26
35
  desc 'Recreate test fixtures'
27
36
  task :fixtures do
28
37
  require 'cineworld_uk'
38
+ CineworldUk.token = ENV.fetch("CINEWORLD_TOKEN")
29
39
 
30
40
  def js_fixture(name)
31
41
  File.expand_path("../test/fixtures/#{name}.json", __FILE__)
@@ -38,45 +48,11 @@ task :fixtures do
38
48
  file.write api.cinema_list
39
49
  end
40
50
 
41
- File.open(js_fixture('api/cinema-detail-3'), 'w') do |file|
42
- puts '* API Cinema Detail Brighton'
43
- file.write api.cinema_detail(3)
44
- end
45
-
46
- File.open(js_fixture('api/cinema-detail-96'), 'w') do |file|
47
- puts '* API Cinema Detail Birmingham NEC'
48
- file.write api.cinema_detail(96)
49
- end
50
-
51
- File.open(js_fixture('api/cinema-detail-21'), 'w') do |file|
52
- puts '* API Cinema Detail Edinburgh'
53
- file.write api.cinema_detail(21)
54
- end
55
-
56
- File.open(js_fixture('api/cinema-detail-10'), 'w') do |file|
57
- puts '* API Cinema Detail Chelsea'
58
- file.write api.cinema_detail(10)
59
- end
60
-
61
- File.open(js_fixture('api/film-list'), 'w') do |file|
62
- puts '* API Film List'
63
- file.write api.film_list
64
- end
65
-
66
- File.open(js_fixture('api/film-list-comingsoon'), 'w') do |file|
67
- puts '* API Film List Coming Soon'
68
- file.write api.film_list_comingsoon
69
- end
70
-
71
- File.open(js_fixture('api/performances-tomorrow-3'), 'w') do |file|
51
+ File.open(js_fixture('api/performances-tomorrow-14'), 'w') do |file|
72
52
  puts '* API Performances in Brighton Tomorrow'
73
- file.write api.performances(3, Date.today + 1)
53
+ file.write api.performances(14, Date.today + 1)
74
54
  end
75
55
 
76
- File.open(js_fixture('api/dates-3'), 'w') do |file|
77
- puts '* API Dates Brighton Tomorrow'
78
- file.write api.dates(3)
79
- end
80
56
  end
81
57
 
82
58
  task :default => :test
data/cineworld_uk.gemspec CHANGED
@@ -12,16 +12,18 @@ 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 'cinebase', '~> 5.0'
28
+ spec.add_runtime_dependency 'nokogiri', '>= 1.11'
27
29
  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::HTTP.build(host: 'www2.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
@@ -7,8 +7,9 @@ module CineworldUk
7
7
  class CinemaAddress
8
8
  # @param [Integer] id the cinema id
9
9
  # @return [CineworldUk::Internal::Parser::Api::CinemaAddress]
10
- def initialize(id)
10
+ def initialize(id, cinemas = nil)
11
11
  @id = id
12
+ @cinemas = cinemas
12
13
  end
13
14
 
14
15
  # @return [Hash] contains :street_address, :extended_address,
@@ -27,52 +28,51 @@ module CineworldUk
27
28
 
28
29
  private
29
30
 
30
- def adr_array
31
- @adr_array ||=
32
- cinema_detail_hash['address'].split(',').map(&:strip).compact
31
+ def address_info
32
+ @address_info ||= cinema_hash['addressInfo'] || {}
33
33
  end
34
34
 
35
- def cinema_detail_hash
36
- @cinema_detail_hash ||= JSON.parse(cinema_detail_response)['cinema']
35
+ def cinema_hash
36
+ @cinema_hash ||= begin
37
+ cinemas = @cinemas ||
38
+ JSON.parse(cinema_list_response)["body"]["cinemas"]
39
+ formatted_id = @id.to_s.rjust(3, '0')
40
+ cinemas.find { |c| c['id'] == formatted_id } || {}
41
+ end
37
42
  end
38
43
 
39
- def cinema_detail_response
40
- @cinema_detail_response ||=
41
- CineworldUk::Internal::ApiResponse.new.cinema_detail(@id)
42
- end
43
-
44
- def ext_array
45
- @ext_array ||= adr_array[1..-1]
44
+ def cinema_list_response
45
+ @cinema_list_response ||=
46
+ CineworldUk::Internal::ApiResponse.new.cinema_list
46
47
  end
47
48
 
48
49
  def extended_address
49
- return nil if ext_array.count == 1
50
- london? ? nil : ext_array[0]
51
- end
50
+ addr2 = address_info['address2']
51
+ addr3 = address_info['address3']
52
52
 
53
- def final
54
- @final ||= ext_array.last
53
+ return nil if london?
54
+ addr2 || addr3
55
55
  end
56
56
 
57
57
  def locality
58
- return ext_array[0] if ext_array.count == 1
59
- london? ? ext_array[0] : ext_array[1]
58
+ address_info['city']
60
59
  end
61
60
 
62
61
  def london?
63
- final == 'London'
62
+ address_info['city'] == 'London' ||
63
+ cinema_hash['displayName']&.start_with?('London')
64
64
  end
65
65
 
66
66
  def postal_code
67
- cinema_detail_hash['postcode']
67
+ address_info['postalCode']
68
68
  end
69
69
 
70
70
  def region
71
- 'London' if london?
71
+ address_info['state'] || ('London' if london?)
72
72
  end
73
73
 
74
74
  def street_address
75
- adr_array[0]
75
+ address_info['address1']
76
76
  end
77
77
  end
78
78
  end