odeon_uk 4.0.0 → 5.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 (72) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +43 -0
  3. data/.gitignore +1 -1
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +33 -0
  6. data/README.md +90 -21
  7. data/Rakefile +14 -15
  8. data/lib/odeon_uk/cinema.rb +41 -23
  9. data/lib/odeon_uk/internal/api_response.rb +19 -39
  10. data/lib/odeon_uk/internal/guest_token.rb +23 -0
  11. data/lib/odeon_uk/internal/parser/api/showtime.rb +80 -0
  12. data/lib/odeon_uk/internal/title_sanitizer.rb +3 -8
  13. data/lib/odeon_uk/performance.rb +34 -40
  14. data/lib/odeon_uk/version.rb +3 -3
  15. data/lib/odeon_uk.rb +16 -2
  16. data/odeon_uk.gemspec +10 -6
  17. data/test/fixtures/api/showtimes-824.json +66 -0
  18. data/test/fixtures/api/sites.json +32 -0
  19. data/test/lib/odeon_uk/cinema_test.rb +66 -215
  20. data/test/lib/odeon_uk/internal/api_response_test.rb +48 -29
  21. data/test/lib/odeon_uk/internal/fixture_shape_test.rb +14 -0
  22. data/test/lib/odeon_uk/internal/guest_token_test.rb +84 -0
  23. data/test/lib/odeon_uk/internal/parser/api/showtime_test.rb +68 -0
  24. data/test/lib/odeon_uk/internal/title_sanitizer_test.rb +109 -89
  25. data/test/lib/odeon_uk/performance_test.rb +124 -123
  26. data/test/lib/odeon_uk/version_test.rb +3 -3
  27. data/test/live/api_shape_test.rb +19 -0
  28. data/test/live/integration_test.rb +21 -0
  29. data/test/live/live_helper.rb +11 -0
  30. data/test/package_test.rb +56 -0
  31. data/test/support/api_fixtures_helper.rb +5 -30
  32. data/test/support/api_shapes.rb +46 -0
  33. data/test/support/fake_api_response.rb +9 -6
  34. data/test/test_helper.rb +6 -3
  35. metadata +66 -89
  36. data/.travis.yml +0 -7
  37. data/lib/odeon_uk/internal/parser/api/film_lookup.rb +0 -34
  38. data/lib/odeon_uk/internal/parser/api/performance_day.rb +0 -92
  39. data/rake/fixture_creator.rb +0 -46
  40. data/test/fixtures/api/all_cinemas.plist +0 -0
  41. data/test/fixtures/api/app_init.plist +0 -0
  42. data/test/fixtures/api/film_times/71-14876.plist +0 -0
  43. data/test/fixtures/api/film_times/71-15408.plist +0 -0
  44. data/test/fixtures/api/film_times/71-15684.plist +0 -0
  45. data/test/fixtures/api/film_times/71-15866.plist +0 -0
  46. data/test/fixtures/api/film_times/71-15868.plist +0 -0
  47. data/test/fixtures/api/film_times/71-15961.plist +0 -0
  48. data/test/fixtures/api/film_times/71-15963.plist +0 -0
  49. data/test/fixtures/api/film_times/71-15965.plist +0 -0
  50. data/test/fixtures/api/film_times/71-15967.plist +0 -0
  51. data/test/fixtures/api/film_times/71-15969.plist +0 -0
  52. data/test/fixtures/api/film_times/71-15977.plist +0 -0
  53. data/test/fixtures/api/film_times/71-15980.plist +0 -0
  54. data/test/fixtures/api/film_times/71-16037.plist +0 -0
  55. data/test/fixtures/api/film_times/71-16136.plist +0 -0
  56. data/test/fixtures/api/film_times/71-16142.plist +0 -0
  57. data/test/fixtures/api/film_times/71-16143.plist +0 -0
  58. data/test/fixtures/api/film_times/71-16187.plist +0 -0
  59. data/test/fixtures/api/film_times/71-16369.plist +0 -0
  60. data/test/fixtures/api/film_times/71-16370.plist +0 -0
  61. data/test/fixtures/api/film_times/71-16374.plist +0 -0
  62. data/test/fixtures/api/film_times/71-16386.plist +0 -0
  63. data/test/fixtures/api/film_times/71-16387.plist +0 -0
  64. data/test/fixtures/api/film_times/71-16388.plist +0 -0
  65. data/test/fixtures/api/film_times/71-16438.plist +0 -0
  66. data/test/fixtures/api/film_times/71-16454.plist +0 -0
  67. data/test/fixtures/api/film_times/71-16457.plist +0 -0
  68. data/test/fixtures/api/film_times/71-16477.plist +0 -0
  69. data/test/fixtures/api/film_times/71-16478.plist +0 -0
  70. data/test/fixtures/api/film_times/71-16480.plist +0 -0
  71. data/test/fixtures/api/film_times/71-16482.plist +0 -0
  72. data/test/fixtures/api/film_times/71-16489.plist +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2cfbaab4a67b19a98fcf71294df4498b7aa003b5
4
- data.tar.gz: c672b4883b91b16fe1dcca96a0591e56aebca34b
2
+ SHA256:
3
+ metadata.gz: 0f8dac2f14a307cda7efcbbe3e2a1806144ae44962cb543a676397beac9128e9
4
+ data.tar.gz: 89240ce98e81acc220c63fcd010e5fbc8468056c41a3d2d1e014dbae5223a4b6
5
5
  SHA512:
6
- metadata.gz: fa4751ab47bd72bcba2e1ab741dbfea0d3d77f67ecb2bbe98bae322f763147ba9fc74442435aadd491e2fde023d08a98c47a85e2b3776cf95cfd2c6f10938e3a
7
- data.tar.gz: f4a35ffc91610b077221e2294f8cedfd0356dd8ef2865ece084b7829a013334738913e0dc717c71041eefac4c80e145e7c904ebcd8fe5a5dcff7b176591a1f6c
6
+ metadata.gz: c03e61f5667184a6992d22582f6ee67e94f4e163ab0d1c4e82132816e7d4f97d46cb1f0a98712ce5bf60b2477f519e150e2107423a4a5df1ca04bcd6e9822b1d
7
+ data.tar.gz: ead7043b142df55fbafedb7dc3b165c35b2082e250ed1e35419e46eb3b1b0826929c76e1c4256461c94861bf128ac52b4bed5b2bc8c6ae85ce75249346498920
@@ -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.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: '4.0'
37
+ bundler-cache: true
38
+ - run: bundle exec rake 'package:native[${{ matrix.platform }}]'
39
+ - run: ODEON_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,4 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
- .DS_Store
18
+ .DS_Storevendor/curl-impersonate
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
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
+ ## 5.0.0 - 2026-10-22
6
+
7
+ ### Changed
8
+ - Retrieve cinemas and performances from the Vista API behind odeon.co.uk instead of the retired iOS plist API.
9
+ - Cinema ids are Vista site ids, zero-padded strings such as `'017'` and `'824'` (Brighton was `71`).
10
+ - `Cinema.all` returns UK cinemas only.
11
+ - `Cinema#adr` includes the second address line. Postcodes are no longer available.
12
+ - `Cinema#url` points at the current cinema pages.
13
+ - `Performance.at` accepts inclusive `from:` and `to:` business dates, defaulting to today in London and the following 13 days. One request per day.
14
+ - Performance times come from Vista's explicit UTC offsets.
15
+ - Title sanitizer handles Met Opera and RBO season prefixes, language and encore suffixes.
16
+ - Require Ruby 3.4 or newer and cinebase 5.2, which now holds the shared Vista client, guest token retrieval, packaging task and common title rules.
17
+ - Film titles drop dub, subtitle, language, anniversary and Q&A markers, and festival prefixes.
18
+ - Convert tests from minitest spec syntax to plain `Minitest::Test` classes.
19
+
20
+ ### Added
21
+ - `OdeonUk.token` for a caller-supplied guest JWT, and `OdeonUk.acquire_token!` to fetch one with `curl-impersonate`.
22
+ - Platform gems that bundle curl-impersonate 2.2.3.
23
+ - `OdeonUk::AuthenticationError` for missing, expired and rejected tokens.
24
+ - `Performance#booking_url`, `Performance#language`, `Performance#subtitled?` and `Performance#festival` (a `Cinebase::Festival` with `name` and `year`).
25
+ - `audio_described`, `autism_friendly` and `subtitled` variants.
26
+ - Live integration tests and GitHub Actions CI on Ruby 3.4 and 4.0.
27
+ - Live API shape tests that check real responses without storing them.
28
+
29
+ ### Removed
30
+ - CFPropertyList dependency, Travis CI and Code Climate.
31
+ - Recorded API fixtures and the `fixtures` rake task. Fixtures are hand-written.
32
+
33
+ ## 4.0.1 - 2016-02-11
34
+
35
+ ### Fixed
36
+ - pass an `to_int`able `String` to methods and live success
37
+
5
38
  ## 4.0.0 - 2016-02-09
6
39
 
7
40
  ### Removed
data/README.md CHANGED
@@ -1,11 +1,8 @@
1
1
  # OdeonUk
2
2
 
3
- A simple gem to parse the [Odeon UK website](http://odeon.co.uk) and spit out useful formatted info.
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
4
 
5
5
  [![Gem Version](https://badge.fury.io/rb/odeon_uk.svg)](https://badge.fury.io/rb/odeon_uk)
6
- [![Code Climate](https://codeclimate.com/github/andycroll/odeon_uk/badges/gpa.svg)](https://codeclimate.com/github/andycroll/odeon_uk)
7
- [![Test Coverage](https://codeclimate.com/github/andycroll/odeon_uk/badges/coverage.svg)](https://codeclimate.com/github/andycroll/odeon_uk/coverage)
8
- [![Build Status](https://travis-ci.org/andycroll/odeon_uk.svg?branch=master)](https://travis-ci.org/andycroll/odeon_uk)
9
6
  [![Inline docs](http://inch-ci.org/github/andycroll/odeon_uk.svg?branch=master)](http://inch-ci.org/github/andycroll/odeon_uk)
10
7
 
11
8
  ## Installation
@@ -26,25 +23,65 @@ Or install it yourself as:
26
23
 
27
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.
28
25
 
29
- 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).
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.
30
63
 
31
64
  ### Cinema
32
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
+
33
70
  ``` ruby
34
71
  OdeonUk::Cinema.all
35
72
  #=> [<OdeonUk::Cinema ...>, <OdeonUk::Cinema ...>, ...]
36
73
 
37
- cinema = OdeonUk::Cinema.new(71)
74
+ cinema = OdeonUk::Cinema.new(824)
38
75
  #=> <OdeonUk::Cinema ...>
39
76
 
40
77
  cinema.adr
41
78
  #=> {
42
79
  street_address: 'Kingswest',
43
- extended_address: nil,
80
+ extended_address: 'West Street',
44
81
  locality: 'Brighton',
45
82
  region: nil,
46
- postal_code: 'BN1 2RE',
47
- country: 'United Kingdom'
83
+ postal_code: nil,
84
+ country_name: 'United Kingdom'
48
85
  }
49
86
 
50
87
  cinema.brand
@@ -54,46 +91,78 @@ cinema.full_name
54
91
  #=> 'Odeon Brighton'
55
92
 
56
93
  cinema.id
57
- #=> 71
94
+ #=> '824'
58
95
 
59
- cinema.postal_code
60
- #=> 'BN1 2RE'
96
+ cinema.url
97
+ #=> 'https://www.odeon.co.uk/cinemas/brighton/'
61
98
  ```
62
99
 
63
100
  ### Performances
64
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
+
65
106
  ``` ruby
66
- OdeonUk::Performance.at(17)
107
+ OdeonUk::Performance.at(824)
67
108
  #=> [<OdeonUk::Performance ...>, <OdeonUk::Performance ...>, ...]
68
109
 
69
- performance = OdeonUk::Performance.at(17).first
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
70
114
  #=> <OdeonUk::Performance ...>
71
115
 
72
116
  performance.film_name
73
- #=> 'Star Wars: The Force Awakens'
117
+ #=> 'Street Fighter'
74
118
 
75
119
  performance.dimension
76
120
  #=> '2d'
77
121
 
78
122
  performance.variant
79
- #=> ['imax', 'kids']
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
80
133
 
81
134
  performance.starting_at
82
- #=> 2016-02-04 13:00:00 UTC
135
+ #=> 2026-10-24 08:00:00 UTC
83
136
 
84
137
  performance.showing_on
85
- #=> #<Date: 2016-02-04 ((2457423j,0s,0n),+0s,2299161j)>
138
+ #=> #<Date: 2026-10-24 ((2461338j,0s,0n),+0s,2299161j)>
86
139
 
87
- performance.booking_url # for now
88
- #=> nil
140
+ performance.booking_url
141
+ #=> 'https://www.odeon.co.uk/ticketing/seat-picker/?showtimeId=824-69206'
89
142
 
90
143
  performance.cinema_name
91
144
  #=> 'Brighton'
92
145
 
93
146
  performance.cinema_id
94
- #=> 71
147
+ #=> '824'
95
148
  ```
96
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
+
97
166
  ## Contributing
98
167
 
99
168
  1. Fork it
data/Rakefile CHANGED
@@ -2,13 +2,25 @@
2
2
  require 'bundler/gem_tasks'
3
3
 
4
4
  require 'rake/testtask'
5
+ require 'cinebase/vista/package_task'
6
+
7
+ Cinebase::Vista::PackageTask.new('odeon_uk.gemspec', root: __dir__)
5
8
 
6
9
  Rake::TestTask.new do |t|
7
10
  t.libs << 'lib/odeon_uk'
8
11
  t.test_files = FileList[
9
12
  'test/lib/odeon_uk/*_test.rb',
10
- 'test/lib/odeon_uk/api/*_test.rb',
11
- 'test/lib/odeon_uk/internal/*_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'
12
24
  ]
13
25
  t.verbose = true
14
26
  end
@@ -23,17 +35,4 @@ task :console do
23
35
  IRB.start
24
36
  end
25
37
 
26
- desc 'recreate test fixtures'
27
- task :fixtures do
28
- require 'odeon_uk'
29
- require_relative 'rake/fixture_creator'
30
- require 'fileutils'
31
-
32
- FixtureCreator::Api.new.app_init!
33
- FixtureCreator::Api.new.all_cinemas!
34
- puts 'Clean folder'
35
- FileUtils.rm FileList['test/fixtures/api/film_times/*.plist']
36
- FixtureCreator::Api.new.film_times!(71)
37
- end
38
-
39
38
  task default: :test
@@ -2,22 +2,28 @@ module OdeonUk
2
2
  # The object representing a cinema on the Odeon UK website
3
3
  class Cinema < Cinebase::Cinema
4
4
  # @!attribute [r] id
5
- # @return [Integer] the numeric id of the cinema on the Cineworld website
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
6
12
 
7
13
  # Constructor
8
- # @param [Integer, String] id the cinema id of the format 71/'71' as used on
14
+ # @param [Integer, String] id the cinema id of the format 17/'017'
9
15
  # @return [OdeonUk::Cinema]
10
16
  def initialize(id)
11
- @id = id.to_s
17
+ @id = id.to_s.rjust(3, '0')
12
18
  end
13
19
 
14
- # Return basic cinema information for all cinemas
20
+ # Return basic cinema information for all UK cinemas
15
21
  # @return [Array<OdeonUk::Cinema>]
16
22
  # @example
17
23
  # OdeonUk::Cinema.all
18
24
  # #=> [<OdeonUk::Cinema>, <OdeonUk::Cinema>, ...]
19
25
  def self.all
20
- cinemas_hash.keys.map { |cinema_id| new(cinema_id) }
26
+ sites_hash.keys.map { |cinema_id| new(cinema_id) }
21
27
  end
22
28
 
23
29
  # @!method address
@@ -29,23 +35,26 @@ module OdeonUk
29
35
  # @return [Hash] contains :street_address, :extended_address,
30
36
  # :locality, :postal_code, :country
31
37
  # @example
32
- # cinema = OdeonUk::Cinema.new(3)
38
+ # cinema = OdeonUk::Cinema.new(824)
33
39
  # cinema.adr
34
40
  # #=> {
35
- # street_address: '44-47 Gardner Street',
36
- # extended_address: 'North Laine',
41
+ # street_address: 'Kingswest',
42
+ # extended_address: 'West Street',
37
43
  # locality: 'Brighton',
38
- # postal_code: 'BN1 1UN',
44
+ # region: nil,
45
+ # postal_code: nil,
39
46
  # country_name: 'United Kingdom'
40
47
  # }
41
48
  # @note Uses the standard method naming as at http://microformats.org/wiki/adr
49
+ # @note The API does not supply postcodes
42
50
  def adr
51
+ address = site_hash.dig('contactDetails', 'address') || {}
43
52
  {
44
- street_address: cinema_hash['siteAddress1'],
45
- extended_address: nil,
46
- locality: cinema_hash['siteAddress2'],
53
+ street_address: address['line1'],
54
+ extended_address: address['line2'],
55
+ locality: address['city'],
47
56
  region: nil,
48
- postal_code: cinema_hash['sitePostcode'],
57
+ postal_code: nil,
49
58
  country_name: 'United Kingdom'.freeze
50
59
  }
51
60
  end
@@ -55,7 +64,7 @@ module OdeonUk
55
64
  # @example
56
65
  # cinema = OdeonUk::Cinema.new(3)
57
66
  # cinema.brand
58
- # #=> 'Cineworld'
67
+ # #=> 'Odeon'
59
68
  def brand
60
69
  'Odeon'.freeze
61
70
  end
@@ -103,7 +112,7 @@ module OdeonUk
103
112
  # cinema.name
104
113
  # #=> 'Brighton'
105
114
  def name
106
- @name ||= cinema_hash['siteName'].gsub(/\ALondon /, '').gsub(' - ', ': ')
115
+ @name ||= site_name.gsub(/\ALondon /, '').gsub(' - ', ': ')
107
116
  end
108
117
 
109
118
  # @!method postal_code
@@ -142,22 +151,31 @@ module OdeonUk
142
151
  # The url of the cinema on the Cineworld website
143
152
  # @return [String]
144
153
  def url
145
- "http://www.odeon.co.uk/cinemas/#{urlized_name}/#{id}/"
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
146
162
  end
163
+ private_class_method :sites_hash
147
164
 
148
165
  private
149
166
 
150
- def self.cinemas_hash
151
- @cinemas_hash ||=
152
- OdeonUk::Internal::ApiResponse.new.all_cinemas.fetch('sites', {})
167
+ def site_hash
168
+ @site_hash ||= self.class.send(:sites_hash).fetch(id, {})
153
169
  end
154
170
 
155
- def cinema_hash
156
- @cinema_hash ||= self.class.cinemas_hash.fetch(id, {})
171
+ def site_name
172
+ site_hash.dig('name', 'text').to_s
157
173
  end
158
174
 
159
- def urlized_name
160
- name.downcase.gsub(/[^a-z0-9]/, '_')
175
+ def url_slug
176
+ URL_SLUGS.fetch(id) do
177
+ site_name.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-|-\z/, '')
178
+ end
161
179
  end
162
180
  end
163
181
  end
@@ -1,55 +1,35 @@
1
- require 'cfpropertylist'
2
- require 'net/http'
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
3
4
 
4
5
  module OdeonUk
5
6
  # Internal utility classes: Do not use
6
7
  # @api private
7
8
  module Internal
8
- # Utility class to make calls to the odeon website
9
+ # Fetches cinema and showtime data from the Vista API behind odeon.co.uk
9
10
  class ApiResponse
10
- # iOS app API version
11
- VERSION = '2.1'.freeze
12
-
13
- # cinemas information
14
- # @return [Hash] decoded response of api containing cinema details
15
- def all_cinemas
16
- parse(all_cinemas_raw)
17
- end
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"
18
14
 
19
- # application initialize
20
- # @return [Hash] decoded response of api, mostly films
21
- def app_init
22
- parse(app_init_raw)
15
+ # @return [Array<Hash>] UK cinemas
16
+ def sites
17
+ client.get("sites").fetch("sites")
23
18
  end
24
19
 
25
- # showings for a film at a cinema
26
- # @return [Hash] decoded response of api, day split times
27
- def film_times(cinema_id, film_id)
28
- parse(film_times_raw(cinema_id, film_id))
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)
29
26
  end
30
27
 
31
28
  private
32
29
 
33
- def all_cinemas_raw
34
- post('all-cinemas').body
35
- end
36
-
37
- def app_init_raw
38
- post('app-init').body
39
- end
40
-
41
- def film_times_raw(cinema_id, film_id)
42
- post('film-times', { s: cinema_id, m: film_id }).body
43
- end
44
-
45
- def post(path, request_body={})
46
- uri = URI("https://api.odeon.co.uk/#{VERSION}/api/#{path}")
47
- Net::HTTP.post_form(uri, request_body)
48
- end
49
-
50
- def parse(content)
51
- plist = CFPropertyList::List.new(data: content)
52
- CFPropertyList.native_types(plist.value).fetch('data', {})
30
+ def client
31
+ Cinebase::Vista::Client.new(base_url: BASE_URL, region_code: REGION_CODE,
32
+ token: OdeonUk.token)
53
33
  end
54
34
  end
55
35
  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
@@ -18,16 +18,11 @@ module OdeonUk
18
18
  # strings and regex to be removed
19
19
  def remove
20
20
  [
21
- /\s+[23][dD]/, # dimension
22
21
  'Autism Friendly Screening -', # autism screening
23
- /\ACinemagic \d{1,4} \-/, # cinemagic
24
22
  /\(encore.+\)/i, # encore for NT Live
25
23
  /\(live\)/i, # live in brackets
26
- 'UKJFF -', # UK Jewish festival prefix
27
- /\bsing\-?a\-?long\b/i, # singalong
28
- 'Autism Friendly - ', # autism friendly
29
- 'amp;', # html ampersands
30
- ]
24
+ 'Autism Friendly - ' # autism friendly
25
+ ] + COMMON_REMOVE
31
26
  end
32
27
 
33
28
  # regexes and their replacements
@@ -40,7 +35,7 @@ module OdeonUk
40
35
  /NT Live - (.*)/ => 'National Theatre: ',
41
36
  /ROH - (.*)/ => 'Royal Opera House: ',
42
37
  /(.*)\- RSC Live \d{1,4}/ => 'Royal Shakespeare Company: '
43
- }
38
+ }.merge(COMMON_REPLACE)
44
39
  end
45
40
  end
46
41
  end