cineworld_uk 4.3.2-x86_64-linux-gnu → 4.5.0-x86_64-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ddd265d5bfbbedaec6d9004a093f70b02873a978ce710f60f0e811b5bc86e7c
4
- data.tar.gz: 39da4663b71cdf540a370f69d152287f851ef285cd497b95955cdf5859d7e1e4
3
+ metadata.gz: b152f11fcf9ae8cc88be932311d9e46c4029a239b59ffa32e952d39b390495bc
4
+ data.tar.gz: b778579ab039423dbf2d51b1a68ae336cadcfbd2ef982446328486b72149306d
5
5
  SHA512:
6
- metadata.gz: a604de2a95540a40e026a4f005a84e6739f2b7d165f6c3e473df5fb97fe53a2fbb2bfd0c3d240154eed13f57cb592387460a8d98e3c6710379397ef10bd8873e
7
- data.tar.gz: ea2ca7e6dd557850ed0726910c95de48aed582d10f60dc33e7c9b5866fb128932504f4d13f7b4d6638ff9368b74b5af98901c3c6b2b6f13f1d42c79959824818
6
+ metadata.gz: 06e4842e31ffac91113eee8770eec53f483eeef93ca07f3f632e1fa9d08408fe4ecc2a2d2ba105d594388128ef21007725797938fa13da7094c1c41d66938fc3
7
+ data.tar.gz: d746059a80b860ed9bb8e2d63d16b3c2e65acbed8db2eda685bc2a7fb78ffa0a4c82220d638bf5d5c28f158572cc8df2a009d470b2a9ad9e5365586a8897490c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
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.5.0] - 2026-11-03
6
+
7
+ ### Changed
8
+ - Uses cinebase 5.9: performances take the raw `title:`, so audience labels become variants, and series prefixes such as `Met Opera:` become the performance's `event`.
9
+ - National Theatre Live screenings are named `National Theatre Live: `, not `National Theatre: `, before cinebase moves the series to `event`.
10
+
11
+ ### Fixed
12
+ - `Met Opera - Cosi Fan Tutte (Mozart)` is now `Cosi Fan Tutte`: a Met Opera title loses a trailing bracketed composer.
13
+
14
+ ## 4.4.0 - 2026-10-31
15
+
16
+ ### Added
17
+ - `Cinema#latitude` and `Cinema#longitude` as Floats in decimal degrees, from the Vista sites response. They are `nil` when a site has no location or sits at 0, 0.
18
+ - `CineworldUk.ensure_token!` fetches a guest token only when there is none or it expires within an hour, and `CineworldUk.token_expires_at` reads its expiry. Both come from cinebase's `Vista::TokenHolder`.
19
+
20
+ ### Changed
21
+ - A token the API rejects is cleared from `CineworldUk.token`, so the next `ensure_token!` fetches a fresh one.
22
+ - Requires cinebase 5.6.
23
+
5
24
  ## 4.3.2 - 2026-10-22
6
25
 
7
26
  ### Changed
data/Gemfile CHANGED
@@ -2,3 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in cineworld_uk.gemspec
4
4
  gemspec
5
+
data/README.md CHANGED
@@ -36,6 +36,16 @@ CineworldUk.acquire_token!
36
36
  CineworldUk::Cinema.all
37
37
  ```
38
38
 
39
+ `acquire_token!` always fetches a new token. `ensure_token!` fetches one only
40
+ when there is no token, or the token expires within an hour, and otherwise
41
+ keeps the current one. Call it before each batch of requests:
42
+
43
+ ```ruby
44
+ CineworldUk.ensure_token!
45
+ CineworldUk.token_expires_at
46
+ #=> 2026-10-31 03:00:00 UTC
47
+ ```
48
+
39
49
  This shells out to **`curl-impersonate`**, not ordinary system `curl`, using its
40
50
  Chrome 150 TLS/HTTP profile. No Python, Chrome, JavaScript execution or browser
41
51
  cookies are needed. Platform-specific gem packages include the pinned
@@ -89,8 +99,9 @@ The gem sends `Authorization: Bearer ...` and `Connect-Region-Code: GB` to
89
99
  automatically.
90
100
 
91
101
  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
102
+ A token the API rejects is cleared from `CineworldUk.token`, so call
103
+ `CineworldUk.ensure_token!`, or set `CineworldUk.token` with a fresh token,
104
+ before retrying. Tokens are never persisted by the gem. Other HTTP
94
105
  failures raise an error rather than returning an empty schedule. Redirects are
95
106
  not followed, so credentials are not forwarded to another host.
96
107
 
@@ -134,6 +145,12 @@ cinema.full_name
134
145
  cinema.id
135
146
  #=> 14
136
147
 
148
+ cinema.latitude
149
+ #=> 50.8125991821289
150
+
151
+ cinema.longitude
152
+ #=> -0.104699999094009
153
+
137
154
  cinema.postal_code
138
155
  #=> 'BN2 5UF'
139
156
  ```
data/cineworld_uk.gemspec CHANGED
@@ -25,6 +25,6 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency 'simplecov', '< 1'
26
26
  spec.add_development_dependency 'webmock'
27
27
 
28
- spec.add_runtime_dependency 'cinebase', '~> 5.5'
28
+ spec.add_runtime_dependency 'cinebase', '~> 5.9'
29
29
  spec.add_runtime_dependency 'nokogiri', '>= 1.11'
30
30
  end
@@ -101,6 +101,26 @@ module CineworldUk
101
101
  # cinema.locality
102
102
  # #=> 'Brighton'
103
103
 
104
+ # Latitude of the cinema
105
+ # @return [Float, nil] decimal degrees, or nil when unknown
106
+ # @example
107
+ # cinema = CineworldUk::Cinema.new(14)
108
+ # cinema.latitude
109
+ # #=> 50.8125991821289
110
+ def latitude
111
+ coordinates&.first
112
+ end
113
+
114
+ # Longitude of the cinema
115
+ # @return [Float, nil] decimal degrees, or nil when unknown
116
+ # @example
117
+ # cinema = CineworldUk::Cinema.new(14)
118
+ # cinema.longitude
119
+ # #=> -0.104699999094009
120
+ def longitude
121
+ coordinates&.last
122
+ end
123
+
104
124
  # The name of the cinema
105
125
  # @return [String]
106
126
  # @example
@@ -164,5 +184,19 @@ module CineworldUk
164
184
  JSON.parse(api.cinema_list)['body']['cinemas']
165
185
  end
166
186
  private_class_method :cinema_list_json
187
+
188
+ private
189
+
190
+ def cinema_hash
191
+ formatted_id = @id.to_s.rjust(3, '0')
192
+ self.class.send(:cinema_list_json).find { |cinema| cinema['id'] == formatted_id } || {}
193
+ end
194
+
195
+ # 0, 0 marks a site whose position was never set
196
+ def coordinates
197
+ location = cinema_hash['location'] || {}
198
+ pair = [location['latitude'], location['longitude']]
199
+ pair.map(&:to_f) if pair.all?(Numeric) && pair.any?(&:nonzero?)
200
+ end
167
201
  end
168
202
  end
@@ -25,6 +25,7 @@ module CineworldUk
25
25
  {
26
26
  "id" => site.fetch("id"),
27
27
  "displayName" => site.fetch("name").fetch("text"),
28
+ "location" => site["location"],
28
29
  "addressInfo" => {
29
30
  "address1" => address.dig("line1", "text"),
30
31
  "city" => address.dig("line2", "text"),
@@ -87,6 +88,9 @@ module CineworldUk
87
88
 
88
89
  def fetch(path, query = {})
89
90
  client.get(path, query)
91
+ rescue Cinebase::Vista::AuthenticationError
92
+ CineworldUk.reject_token!(@client_token)
93
+ raise
90
94
  end
91
95
 
92
96
  # One client keeps one connection open, so reuse it until the token changes.
@@ -24,21 +24,25 @@ module CineworldUk
24
24
  # @param [Integer] year edition when the title gives none
25
25
  # @return [Cinebase::Festival, nil]
26
26
  def festival(year)
27
- Cinebase::Festival.from_title(@data['name'] || '', year: year)
27
+ Cinebase::Festival.from_title(title, year: year)
28
28
  end
29
29
 
30
30
  # @return [Integer, nil] the year at the end of the title
31
31
  def film_year
32
- Cinebase::TitleSanitizer.film_year(@data['name'] || '')
32
+ Cinebase::TitleSanitizer.film_year(title)
33
33
  end
34
34
 
35
35
  # Sanitized film name
36
36
  # @return [String]
37
37
  def name
38
- title = @data['name'] || ''
39
38
  TitleSanitizer.new(title).sanitized
40
39
  end
41
40
 
41
+ # @return [String] the title as listed
42
+ def title
43
+ @data['name'] || ''
44
+ end
45
+
42
46
  # List of strings representing different kinds of performance, such
43
47
  # as autism, kids, imax, members or q&a
44
48
  # @return [Array<String>] or an empty array
@@ -34,7 +34,8 @@ module CineworldUk
34
34
  'SciScreen: ', # SciScreen
35
35
  /\s(-\s)*(With)*\sLive Q And A/i, # Live Q&A
36
36
  /(\s+\-\s+)?MOVIES FOR JUNIORS/i, # movies for juniors
37
- %r{\s20\d{2}[\/-]20\d{2} Season\b}
37
+ %r{\s20\d{2}[\/-]20\d{2} Season\b},
38
+ /\A(?i:Met Opera)\b.*\K\s*\(\p{Lu}[\p{L}-]+\)\z/ # Met Opera composer
38
39
  ] + COMMON_REMOVE
39
40
  end
40
41
 
@@ -87,6 +87,7 @@ module CineworldUk
87
87
  film_year: film.film_year,
88
88
  language: performance.language,
89
89
  starting_at: starting_at,
90
+ title: film.title,
90
91
  variant: (performance.variant + film.variant).sort
91
92
  }
92
93
  end
@@ -1,6 +1,6 @@
1
1
  # Ruby interface for http://www.cineworld.co.uk
2
- # @version 4.3.2
2
+ # @version 4.5.0
3
3
  module CineworldUk
4
4
  # Gem version
5
- VERSION = "4.3.2".freeze
5
+ VERSION = "4.5.0".freeze
6
6
  end
data/lib/cineworld_uk.rb CHANGED
@@ -20,13 +20,10 @@ require_relative './cineworld_uk/performance'
20
20
  module CineworldUk
21
21
  AuthenticationError = Cinebase::Vista::AuthenticationError
22
22
 
23
- # A raw guest JWT, without the "Bearer " prefix. Never persisted by the gem.
24
- class << self
25
- attr_accessor :token
26
-
27
- # Explicit acquisition/renewal; ordinary API calls never launch a subprocess.
28
- def acquire_token!(curl: nil)
29
- self.token = Internal::GuestToken.new(curl: curl).fetch
30
- end
23
+ extend Cinebase::Vista::TokenHolder
24
+
25
+ # @api private
26
+ def self.guest_token_class
27
+ Internal::GuestToken
31
28
  end
32
29
  end
@@ -3,6 +3,7 @@
3
3
  {
4
4
  "id": "014",
5
5
  "name": { "text": "Brighton" },
6
+ "location": { "latitude": 50.8125991821289, "longitude": -0.104699999094009 },
6
7
  "contactDetails": {
7
8
  "addressV2": {
8
9
  "line1": { "text": "1 Example Marina" },
@@ -15,6 +16,7 @@
15
16
  {
16
17
  "id": "088",
17
18
  "name": { "text": "Glasgow Silverburn" },
19
+ "location": { "latitude": 0, "longitude": 0 },
18
20
  "contactDetails": {
19
21
  "addressV2": {
20
22
  "line1": { "text": "2 Example Centre" },
@@ -27,6 +29,7 @@
27
29
  {
28
30
  "id": "103",
29
31
  "name": { "text": "Leicester Square" },
32
+ "location": null,
30
33
  "contactDetails": {
31
34
  "addressV2": {
32
35
  "line1": { "text": "3 Example Square" },
@@ -61,6 +61,25 @@ class CineworldUk::CinemaTest < Minitest::Test
61
61
  assert_equal 'Example: Riverside', CineworldUk::Cinema.new(999).name
62
62
  end
63
63
 
64
+ def test_latitude_and_longitude
65
+ cinema = CineworldUk::Cinema.new(14)
66
+
67
+ assert_in_delta 50.8126, cinema.latitude, 0.0001
68
+ assert_in_delta(-0.1047, cinema.longitude, 0.0001)
69
+ end
70
+
71
+ def test_latitude_and_longitude_are_nil_without_a_location
72
+ [103, 999].each do |id|
73
+ assert_nil CineworldUk::Cinema.new(id).latitude
74
+ assert_nil CineworldUk::Cinema.new(id).longitude
75
+ end
76
+ end
77
+
78
+ def test_latitude_and_longitude_are_nil_at_zero_zero
79
+ assert_nil CineworldUk::Cinema.new(88).latitude
80
+ assert_nil CineworldUk::Cinema.new(88).longitude
81
+ end
82
+
64
83
  def test_locality
65
84
  assert_equal 'Brighton', CineworldUk::Cinema.new(14).locality
66
85
  assert_equal 'London', CineworldUk::Cinema.new(103).locality
@@ -68,6 +68,15 @@ class CineworldUk::Internal::ApiResponseTest < Minitest::Test
68
68
  refute_includes error.message, "expired-secret"
69
69
  end
70
70
 
71
+ def test_rejected_token_is_cleared
72
+ CineworldUk.token = "rejected-token"
73
+ stub_request(:get, "#{BASE_URL}sites").to_return(status: 401)
74
+
75
+ assert_raises(CineworldUk::AuthenticationError) { CineworldUk::Cinema.all }
76
+
77
+ assert_nil CineworldUk.token
78
+ end
79
+
71
80
  def test_uses_replacement_token
72
81
  CineworldUk.token = "replacement"
73
82
  stub_request(:get, "#{BASE_URL}sites")
@@ -27,6 +27,16 @@ class CineworldUk::Internal::GuestTokenTest < Minitest::Test
27
27
  end
28
28
  end
29
29
 
30
+ def test_ensure_token_fetches_only_without_a_current_token
31
+ CineworldUk.token = nil
32
+ html, token = token_page(Time.now.to_i + 7200)
33
+ serve_token_page(html)
34
+
35
+ assert_equal token, CineworldUk.ensure_token!(curl: @curl)
36
+ serve_token_page("", exit_code: 7)
37
+ assert_equal token, CineworldUk.ensure_token!(curl: @curl)
38
+ end
39
+
30
40
  def test_rejects_expired_tokens_including_boundary_and_keeps_current_token
31
41
  now = Time.at(1_800_000_000)
32
42
  Time.stub(:now, now) do
@@ -42,17 +42,21 @@ class CineworldUk::Internal::TitleSanitizerTest < Minitest::Test
42
42
  assert_sanitized 'Met Opera: Le Nozze Di Figaro', 'MET Opera - Le Nozze Di Figaro'
43
43
  end
44
44
 
45
+ def test_met_opera_drops_composer
46
+ assert_sanitized 'Met Opera: Cosi Fan Tutte', 'Met Opera - Cosi Fan Tutte (Mozart)'
47
+ end
48
+
45
49
  def test_nt_live_encore
46
- assert_sanitized 'National Theatre: Frankenstein (starring Benedict Cumberbatch)',
50
+ assert_sanitized 'National Theatre Live: Frankenstein (starring Benedict Cumberbatch)',
47
51
  'NT Live Encore: Frankenstein (starring Benedict Cumberbatch)'
48
52
  end
49
53
 
50
54
  def test_nt_live
51
- assert_sanitized 'National Theatre: Treasure Island', 'NT Live: Treasure Island'
55
+ assert_sanitized 'National Theatre Live: Treasure Island', 'NT Live: Treasure Island'
52
56
  end
53
57
 
54
58
  def test_nt_live_with_encore_suffix
55
- assert_sanitized 'National Theatre: A Streetcar Named Desire (Young Vic)',
59
+ assert_sanitized 'National Theatre Live: A Streetcar Named Desire (Young Vic)',
56
60
  'NT Live: A Streetcar Named Desire (Young Vic) - Encore'
57
61
  end
58
62
 
@@ -70,6 +70,16 @@ class CineworldUk::PerformanceTest < Minitest::Test
70
70
  assert_equal Cinebase::Festival.new(name: 'Cine-Excess', year: 2026), performance.festival
71
71
  end
72
72
 
73
+ def test_at_names_event_from_title
74
+ stub_showtimes(DATE, showtimes_json.sub('The Example Film', 'Met Opera - Cosi Fan Tutte (Mozart)'))
75
+ performance = CineworldUk::Performance.at(14, from: DATE, to: DATE).first
76
+
77
+ assert_equal 'Met Opera - Cosi Fan Tutte (Mozart)', performance.title
78
+ assert_equal 'Cosi Fan Tutte', performance.film_name
79
+ assert_equal Cinebase::Event.new(name: 'Met Opera'), performance.event
80
+ assert_equal %w(arts audio_described), performance.variant
81
+ end
82
+
73
83
  def test_at_parses_format_and_film_year
74
84
  performance = performances_on_date[5]
75
85
 
@@ -8,6 +8,16 @@ class LiveCinemaTest < Minitest::Test
8
8
  assert_operator cinemas.size, :>, 18
9
9
  assert_equal 'Brighton', cinemas.detect { |cinema| cinema.id == 14 }.name
10
10
  end
11
+
12
+ def test_cinemas_have_uk_coordinates
13
+ located = CineworldUk::Cinema.all.select(&:latitude)
14
+
15
+ refute_empty located
16
+ located.each do |cinema|
17
+ assert_includes 49.0..61.0, cinema.latitude, "#{cinema.id} #{cinema.name}"
18
+ assert_includes(-9.0..2.0, cinema.longitude, "#{cinema.id} #{cinema.name}")
19
+ end
20
+ end
11
21
  end
12
22
 
13
23
  class LivePerformanceTest < Minitest::Test
@@ -16,6 +16,11 @@ module ApiShapes
16
16
  line = address.fetch(key)
17
17
  assert_kind_of String, line.fetch('text'), "site #{site['id']} #{key}" unless line.nil?
18
18
  end
19
+ location = site['location'] || {}
20
+ %w(latitude longitude).each do |key|
21
+ value = location[key]
22
+ assert_kind_of Numeric, value, "site #{site['id']} #{key}" unless value.nil?
23
+ end
19
24
  end
20
25
  end
21
26
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cineworld_uk
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.2
4
+ version: 4.5.0
5
5
  platform: x86_64-linux-gnu
6
6
  authors:
7
7
  - Andy Croll
@@ -113,14 +113,14 @@ dependencies:
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '5.5'
116
+ version: '5.9'
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: '5.5'
123
+ version: '5.9'
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: nokogiri
126
126
  requirement: !ruby/object:Gem::Requirement