odeon_uk 4.0.1 → 5.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 (73) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/release.yml +73 -0
  3. data/.github/workflows/test.yml +43 -0
  4. data/.gitignore +1 -1
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +36 -0
  7. data/README.md +100 -21
  8. data/Rakefile +14 -15
  9. data/lib/odeon_uk/cinema.rb +40 -22
  10. data/lib/odeon_uk/internal/api_response.rb +19 -39
  11. data/lib/odeon_uk/internal/guest_token.rb +23 -0
  12. data/lib/odeon_uk/internal/parser/api/showtime.rb +81 -0
  13. data/lib/odeon_uk/internal/title_sanitizer.rb +3 -8
  14. data/lib/odeon_uk/performance.rb +34 -41
  15. data/lib/odeon_uk/version.rb +3 -3
  16. data/lib/odeon_uk.rb +16 -2
  17. data/odeon_uk.gemspec +10 -6
  18. data/test/fixtures/api/showtimes-824.json +66 -0
  19. data/test/fixtures/api/sites.json +32 -0
  20. data/test/lib/odeon_uk/cinema_test.rb +66 -215
  21. data/test/lib/odeon_uk/internal/api_response_test.rb +48 -29
  22. data/test/lib/odeon_uk/internal/fixture_shape_test.rb +14 -0
  23. data/test/lib/odeon_uk/internal/guest_token_test.rb +84 -0
  24. data/test/lib/odeon_uk/internal/parser/api/showtime_test.rb +77 -0
  25. data/test/lib/odeon_uk/internal/title_sanitizer_test.rb +109 -89
  26. data/test/lib/odeon_uk/performance_test.rb +129 -146
  27. data/test/lib/odeon_uk/version_test.rb +3 -3
  28. data/test/live/api_shape_test.rb +19 -0
  29. data/test/live/integration_test.rb +21 -0
  30. data/test/live/live_helper.rb +11 -0
  31. data/test/package_test.rb +56 -0
  32. data/test/support/api_fixtures_helper.rb +5 -30
  33. data/test/support/api_shapes.rb +46 -0
  34. data/test/support/fake_api_response.rb +9 -6
  35. data/test/test_helper.rb +6 -3
  36. metadata +67 -89
  37. data/.travis.yml +0 -7
  38. data/lib/odeon_uk/internal/parser/api/film_lookup.rb +0 -34
  39. data/lib/odeon_uk/internal/parser/api/performance_day.rb +0 -92
  40. data/rake/fixture_creator.rb +0 -46
  41. data/test/fixtures/api/all_cinemas.plist +0 -0
  42. data/test/fixtures/api/app_init.plist +0 -0
  43. data/test/fixtures/api/film_times/71-14876.plist +0 -0
  44. data/test/fixtures/api/film_times/71-15408.plist +0 -0
  45. data/test/fixtures/api/film_times/71-15684.plist +0 -0
  46. data/test/fixtures/api/film_times/71-15866.plist +0 -0
  47. data/test/fixtures/api/film_times/71-15868.plist +0 -0
  48. data/test/fixtures/api/film_times/71-15961.plist +0 -0
  49. data/test/fixtures/api/film_times/71-15963.plist +0 -0
  50. data/test/fixtures/api/film_times/71-15965.plist +0 -0
  51. data/test/fixtures/api/film_times/71-15967.plist +0 -0
  52. data/test/fixtures/api/film_times/71-15969.plist +0 -0
  53. data/test/fixtures/api/film_times/71-15977.plist +0 -0
  54. data/test/fixtures/api/film_times/71-15980.plist +0 -0
  55. data/test/fixtures/api/film_times/71-16037.plist +0 -0
  56. data/test/fixtures/api/film_times/71-16136.plist +0 -0
  57. data/test/fixtures/api/film_times/71-16142.plist +0 -0
  58. data/test/fixtures/api/film_times/71-16143.plist +0 -0
  59. data/test/fixtures/api/film_times/71-16187.plist +0 -0
  60. data/test/fixtures/api/film_times/71-16369.plist +0 -0
  61. data/test/fixtures/api/film_times/71-16370.plist +0 -0
  62. data/test/fixtures/api/film_times/71-16374.plist +0 -0
  63. data/test/fixtures/api/film_times/71-16386.plist +0 -0
  64. data/test/fixtures/api/film_times/71-16387.plist +0 -0
  65. data/test/fixtures/api/film_times/71-16388.plist +0 -0
  66. data/test/fixtures/api/film_times/71-16438.plist +0 -0
  67. data/test/fixtures/api/film_times/71-16454.plist +0 -0
  68. data/test/fixtures/api/film_times/71-16457.plist +0 -0
  69. data/test/fixtures/api/film_times/71-16477.plist +0 -0
  70. data/test/fixtures/api/film_times/71-16478.plist +0 -0
  71. data/test/fixtures/api/film_times/71-16480.plist +0 -0
  72. data/test/fixtures/api/film_times/71-16482.plist +0 -0
  73. 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: 1b827e99f44e531f573ee56fc4139cfa7224a6f6
4
- data.tar.gz: 97351d6e122f0b61602d344ca85ba8a4998b5bfe
2
+ SHA256:
3
+ metadata.gz: 5e46bf230ffd62f78eb2e8f59afa812a90e70ee54fda448688d0b7bc5f6ce662
4
+ data.tar.gz: 4bbd4f71437a569e87020ad2c0e949e4f09d5a0f2f02190b8d321a92edeb802b
5
5
  SHA512:
6
- metadata.gz: bc897b79cea4d4a75bbba73d122243f0dd162c7f0449c5aee13dc0c834a6c57cf092ceaf9479bd672ffbac3ec1708d2a4288542e45d7f766dedd9882bc8e5854
7
- data.tar.gz: 55fb07a9599281fd335da71a188827b8c1b98335fefd8fc0184722a3c2a35d6bd8c5db434439d372b3c1dc1327254e21ef516b4603807d373076c0862a530110
6
+ metadata.gz: 3d4341282698458b67ab5525a22cb332d96213f087a78e3e2f1637175a50d9c5b09f36e4d9f80b74d21f8fdb652ecc6613779085a3ccf0f869c65b803d65564a
7
+ data.tar.gz: 5815d8608c962b9815a50b80439c95040e9dff8aeee16eeb45c8e6dd9137c12c3f790b344163eb8d5674e9a0414f0dc519675e38a1080ec9e4df4be78bf78036
@@ -0,0 +1,73 @@
1
+ name: Release Gem
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ package:
8
+ strategy:
9
+ matrix:
10
+ include:
11
+ - runner: ubuntu-latest
12
+ platform: x86_64-linux-gnu
13
+ - runner: ubuntu-24.04-arm
14
+ platform: aarch64-linux-gnu
15
+ - runner: macos-15
16
+ platform: arm64-darwin
17
+ - runner: macos-15-intel
18
+ platform: x86_64-darwin
19
+ runs-on: ${{ matrix.runner }}
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: '4.0'
25
+ bundler-cache: true
26
+ - run: bundle exec rake 'package:native[${{ matrix.platform }}]'
27
+ - run: ODEON_PACKAGE="$(echo pkg/*.gem)" bundle exec ruby test/package_test.rb
28
+ - uses: actions/upload-artifact@v4
29
+ with:
30
+ name: gem-${{ matrix.platform }}
31
+ path: pkg/*.gem
32
+
33
+ release:
34
+ needs: package
35
+ runs-on: ubuntu-latest
36
+ permissions:
37
+ id-token: write
38
+ contents: write
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: ruby/setup-ruby@v1
42
+ with:
43
+ ruby-version: '4.0'
44
+ bundler-cache: true
45
+ - run: bundle exec rake test
46
+ - run: bundle exec rake build
47
+ - uses: actions/download-artifact@v4
48
+ with:
49
+ path: pkg
50
+ pattern: gem-*
51
+ merge-multiple: true
52
+ - uses: rubygems/configure-rubygems-credentials@v1.0.0
53
+ - name: Push the source and platform gems
54
+ run: |
55
+ for gem in pkg/*.gem; do
56
+ if output=$(gem push "$gem" 2>&1); then
57
+ echo "$output"
58
+ elif echo "$output" | grep -q "Repushing of gem versions is not allowed"; then
59
+ echo "Already published: $gem"
60
+ else
61
+ echo "$output"
62
+ exit 1
63
+ fi
64
+ done
65
+ - name: Tag the release
66
+ run: |
67
+ tag="v$(ruby -e 'print Gem::Specification.load(Dir["*.gemspec"].first).version')"
68
+ if git ls-remote --exit-code --tags origin "$tag" >/dev/null; then
69
+ echo "$tag already exists"
70
+ else
71
+ git tag "$tag"
72
+ git push origin "$tag"
73
+ fi
@@ -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,42 @@
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.1.0 - 2026-10-22
6
+
7
+ ### Added
8
+ - `Performance#film_year`, read from a trailing `(1996)` in the title.
9
+
10
+ ### Changed
11
+ - Require cinebase 5.4. Film titles lose their year suffix, `(Re-release)` and Lithuanian markers, `+ ... Q&A` and `+ Discussion` add-ons, `(35mm)`, `(70mm)` and `4K` markers; `Horror Season` and `CFF:` prefixes become festivals.
12
+
13
+ ## 5.0.0 - 2026-10-22
14
+
15
+ ### Changed
16
+ - Retrieve cinemas and performances from the Vista API behind odeon.co.uk instead of the retired iOS plist API.
17
+ - Cinema ids are Vista site ids, zero-padded strings such as `'017'` and `'824'` (Brighton was `71`).
18
+ - `Cinema.all` returns UK cinemas only.
19
+ - `Cinema#adr` includes the second address line. Postcodes are no longer available.
20
+ - `Cinema#url` points at the current cinema pages.
21
+ - `Performance.at` accepts inclusive `from:` and `to:` business dates, defaulting to today in London and the following 13 days. One request per day.
22
+ - Performance times come from Vista's explicit UTC offsets.
23
+ - Title sanitizer handles Met Opera and RBO season prefixes, language and encore suffixes.
24
+ - 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.
25
+ - Film titles drop dub, subtitle, language, anniversary and Q&A markers, and festival prefixes.
26
+ - Convert tests from minitest spec syntax to plain `Minitest::Test` classes.
27
+
28
+ ### Added
29
+ - `OdeonUk.token` for a caller-supplied guest JWT, and `OdeonUk.acquire_token!` to fetch one with `curl-impersonate`.
30
+ - Platform gems that bundle curl-impersonate 2.2.3.
31
+ - `OdeonUk::AuthenticationError` for missing, expired and rejected tokens.
32
+ - `Performance#booking_url`, `Performance#language`, `Performance#subtitled?` and `Performance#festival` (a `Cinebase::Festival` with `name` and `year`).
33
+ - `audio_described`, `autism_friendly` and `subtitled` variants.
34
+ - Live integration tests and GitHub Actions CI on Ruby 3.4 and 4.0.
35
+ - Live API shape tests that check real responses without storing them.
36
+
37
+ ### Removed
38
+ - CFPropertyList dependency, Travis CI and Code Climate.
39
+ - Recorded API fixtures and the `fixtures` rake task. Fixtures are hand-written.
40
+
5
41
  ## 4.0.1 - 2016-02-11
6
42
 
7
43
  ### Fixed
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,88 @@ 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)
108
+ #=> [<OdeonUk::Performance ...>, <OdeonUk::Performance ...>, ...]
109
+
110
+ OdeonUk::Performance.at(824, from: Date.today, to: Date.today + 2)
67
111
  #=> [<OdeonUk::Performance ...>, <OdeonUk::Performance ...>, ...]
68
112
 
69
- performance = OdeonUk::Performance.at(17).first
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.film_year
126
+ #=> 1996 # from the title, nil when it has no year
127
+
128
+ performance.language
129
+ #=> 'Korean' # audio language, nil when the cinema does not tag one
130
+
131
+ performance.subtitled?
132
+ #=> true # English subtitles or open captions
133
+
134
+ performance.festival
135
+ #=> #<data Cinebase::Festival name="Cine-Excess", year=2026> # nil outside festivals
80
136
 
81
137
  performance.starting_at
82
- #=> 2016-02-04 13:00:00 UTC
138
+ #=> 2026-10-24 08:00:00 UTC
83
139
 
84
140
  performance.showing_on
85
- #=> #<Date: 2016-02-04 ((2457423j,0s,0n),+0s,2299161j)>
141
+ #=> #<Date: 2026-10-24 ((2461338j,0s,0n),+0s,2299161j)>
86
142
 
87
- performance.booking_url # for now
88
- #=> nil
143
+ performance.booking_url
144
+ #=> 'https://www.odeon.co.uk/ticketing/seat-picker/?showtimeId=824-69206'
89
145
 
90
146
  performance.cinema_name
91
147
  #=> 'Brighton'
92
148
 
93
149
  performance.cinema_id
94
- #=> 71
150
+ #=> '824'
95
151
  ```
96
152
 
153
+ Variants are `arts`, `audio_described`, `autism_friendly`, `baby`, `imax`,
154
+ `kids`, `senior` and `subtitled`.
155
+
156
+ ## Tests
157
+
158
+ ``` sh
159
+ bundle exec rake # unit tests against fixtures
160
+ bundle exec rake live # live API, needs curl-impersonate or ODEON_TOKEN
161
+ ```
162
+
163
+ Fixtures in `test/fixtures/api` are hand-written: invented films and
164
+ addresses in the shape of Vista responses, never recorded API output.
165
+ `test/support/api_shapes.rb` describes the fields the gem reads. The unit suite
166
+ checks the fixtures against it and the live suite checks real responses, held
167
+ only in memory. When the API changes, update the shape first, then the fixtures.
168
+
169
+ ## Releasing
170
+
171
+ To release, bump the version, merge, then run the **Release Gem** workflow in
172
+ GitHub Actions. It builds the source and four platform gems, publishes them
173
+ through RubyGems trusted publishing (no MFA code needed) and tags the release.
174
+ Versions already on RubyGems are skipped, so a failed run can be rerun.
175
+
97
176
  ## Contributing
98
177
 
99
178
  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
@@ -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