odeon_uk 5.0.0-aarch64-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 +7 -0
- data/.github/workflows/test.yml +43 -0
- data/.gitignore +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +132 -0
- data/COMM-LICENSE +22 -0
- data/Gemfile +4 -0
- data/LICENSE +617 -0
- data/README.md +174 -0
- data/Rakefile +38 -0
- data/lib/odeon_uk/cinema.rb +181 -0
- data/lib/odeon_uk/internal/api_response.rb +36 -0
- data/lib/odeon_uk/internal/guest_token.rb +23 -0
- data/lib/odeon_uk/internal/parser/api/showtime.rb +80 -0
- data/lib/odeon_uk/internal/title_sanitizer.rb +42 -0
- data/lib/odeon_uk/performance.rb +81 -0
- data/lib/odeon_uk/version.rb +6 -0
- data/lib/odeon_uk.rb +30 -0
- data/odeon_uk.gemspec +31 -0
- data/test/fixtures/api/showtimes-824.json +66 -0
- data/test/fixtures/api/sites.json +32 -0
- data/test/lib/odeon_uk/cinema_test.rb +113 -0
- data/test/lib/odeon_uk/internal/api_response_test.rb +66 -0
- data/test/lib/odeon_uk/internal/fixture_shape_test.rb +14 -0
- data/test/lib/odeon_uk/internal/guest_token_test.rb +84 -0
- data/test/lib/odeon_uk/internal/parser/api/showtime_test.rb +68 -0
- data/test/lib/odeon_uk/internal/title_sanitizer_test.rb +141 -0
- data/test/lib/odeon_uk/performance_test.rb +150 -0
- data/test/lib/odeon_uk/version_test.rb +7 -0
- data/test/live/api_shape_test.rb +19 -0
- data/test/live/integration_test.rb +21 -0
- data/test/live/live_helper.rb +11 -0
- data/test/package_test.rb +56 -0
- data/test/support/api_fixtures_helper.rb +16 -0
- data/test/support/api_shapes.rb +46 -0
- data/test/support/fake_api_response.rb +20 -0
- data/test/test_helper.rb +20 -0
- data/vendor/curl-impersonate/SOURCE +5 -0
- data/vendor/curl-impersonate/bin/curl-impersonate +0 -0
- data/vendor/curl-impersonate/licenses/LICENSE +21 -0
- data/vendor/curl-impersonate/licenses/LICENSE_BORINGSSL +238 -0
- data/vendor/curl-impersonate/licenses/LICENSE_BROTLI +19 -0
- data/vendor/curl-impersonate/licenses/LICENSE_CARES +24 -0
- data/vendor/curl-impersonate/licenses/LICENSE_CURL +22 -0
- data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_GPL2 +339 -0
- data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_GPL3 +35 -0
- data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_LGPL3 +165 -0
- data/vendor/curl-impersonate/licenses/LICENSE_LIBIDN2_UNICODE +91 -0
- data/vendor/curl-impersonate/licenses/LICENSE_NGHTTP2 +23 -0
- data/vendor/curl-impersonate/licenses/LICENSE_NGHTTP3 +22 -0
- data/vendor/curl-impersonate/licenses/LICENSE_NGTCP2 +22 -0
- data/vendor/curl-impersonate/licenses/LICENSE_ZLIB +22 -0
- data/vendor/curl-impersonate/licenses/LICENSE_ZSTD +30 -0
- metadata +223 -0
data/odeon_uk.gemspec
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
+
require 'odeon_uk/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'odeon_uk'
|
|
7
|
+
spec.version = OdeonUk::VERSION
|
|
8
|
+
spec.authors = ['Andy Croll']
|
|
9
|
+
spec.email = ['andy@goodscary.com']
|
|
10
|
+
spec.description = 'Pull movie & cinema information from the ODEON Vista API'
|
|
11
|
+
spec.summary = "It's a scraper, but a nice one"
|
|
12
|
+
spec.homepage = 'https://github.com/andycroll/odeon_uk'
|
|
13
|
+
spec.licenses = %w(AGPL MIT)
|
|
14
|
+
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.files = `git ls-files`.split("\n")
|
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
20
|
+
spec.require_paths = ['lib']
|
|
21
|
+
|
|
22
|
+
spec.add_development_dependency 'base64'
|
|
23
|
+
spec.add_development_dependency 'bundler', '>= 2.0'
|
|
24
|
+
spec.add_development_dependency 'minitest-mock'
|
|
25
|
+
spec.add_development_dependency 'minitest-reporters'
|
|
26
|
+
spec.add_development_dependency 'rake'
|
|
27
|
+
spec.add_development_dependency 'simplecov', '< 1'
|
|
28
|
+
spec.add_development_dependency 'webmock'
|
|
29
|
+
|
|
30
|
+
spec.add_runtime_dependency 'cinebase', '~> 5.2'
|
|
31
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"businessDate": "2026-10-24",
|
|
3
|
+
"showtimes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "824-1001",
|
|
6
|
+
"filmId": "FILM-1",
|
|
7
|
+
"schedule": { "startsAt": "2026-10-24T09:00:00+01:00" },
|
|
8
|
+
"attributeIds": ["A-AD"],
|
|
9
|
+
"requires3dGlasses": false
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "824-1002",
|
|
13
|
+
"filmId": "FILM-1",
|
|
14
|
+
"schedule": { "startsAt": "2026-10-24T14:30:00+01:00" },
|
|
15
|
+
"attributeIds": ["A-3D", "A-IMAX"],
|
|
16
|
+
"requires3dGlasses": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "824-1003",
|
|
20
|
+
"filmId": "FILM-2",
|
|
21
|
+
"schedule": { "startsAt": "2026-10-24T18:00:00+01:00" },
|
|
22
|
+
"attributeIds": ["A-KOREAN", "A-SUB", "A-EVENT"],
|
|
23
|
+
"requires3dGlasses": false
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "824-1004",
|
|
27
|
+
"filmId": "FILM-3",
|
|
28
|
+
"schedule": { "startsAt": "2026-10-24T20:15:00+01:00" },
|
|
29
|
+
"attributeIds": ["A-TAMIL", "A-SUB"],
|
|
30
|
+
"requires3dGlasses": false
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "824-1005",
|
|
34
|
+
"filmId": "FILM-4",
|
|
35
|
+
"schedule": { "startsAt": "2026-10-24T10:00:00+01:00" },
|
|
36
|
+
"attributeIds": ["A-KIDS", "A-CAPT"],
|
|
37
|
+
"requires3dGlasses": false
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "824-1006",
|
|
41
|
+
"filmId": "FILM-1",
|
|
42
|
+
"schedule": { "startsAt": "2026-10-25T00:15:00+01:00" },
|
|
43
|
+
"attributeIds": [],
|
|
44
|
+
"requires3dGlasses": false
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"relatedData": {
|
|
48
|
+
"films": [
|
|
49
|
+
{ "id": "FILM-1", "title": { "text": "The Example Film 3D" } },
|
|
50
|
+
{ "id": "FILM-2", "title": { "text": "Made Up Concert: Live" } },
|
|
51
|
+
{ "id": "FILM-3", "title": { "text": "Imaginary Picture (Tamil)" } },
|
|
52
|
+
{ "id": "FILM-4", "title": { "text": "Pretend Pups" } }
|
|
53
|
+
],
|
|
54
|
+
"attributes": [
|
|
55
|
+
{ "id": "A-AD", "shortName": { "text": "AudioDes" }, "name": { "text": "Audio Described" } },
|
|
56
|
+
{ "id": "A-3D", "shortName": { "text": "3D" }, "name": { "text": "3D" } },
|
|
57
|
+
{ "id": "A-IMAX", "shortName": { "text": "IMAX" }, "name": { "text": "IMAX" } },
|
|
58
|
+
{ "id": "A-KOREAN", "shortName": { "text": "KoreanLang" }, "name": { "text": "Korean (Audio)" } },
|
|
59
|
+
{ "id": "A-TAMIL", "shortName": { "text": "TamilA" }, "name": { "text": "Tamil (Audio)" } },
|
|
60
|
+
{ "id": "A-SUB", "shortName": { "text": "EnglishSub" }, "name": { "text": "English (Sub)" } },
|
|
61
|
+
{ "id": "A-EVENT", "shortName": { "text": "EventCinem" }, "name": { "text": "Event Cinema" } },
|
|
62
|
+
{ "id": "A-KIDS", "shortName": { "text": "ODEON Kids" }, "name": { "text": "ODEON Kids" } },
|
|
63
|
+
{ "id": "A-CAPT", "shortName": { "text": "OpenCapt" }, "name": { "text": "Open Captioned" } }
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sites": [
|
|
3
|
+
{
|
|
4
|
+
"id": "017",
|
|
5
|
+
"name": { "text": "Birmingham New Street" },
|
|
6
|
+
"contactDetails": {
|
|
7
|
+
"address": { "line1": "1 Example Arcade", "line2": "New Street", "city": "Birmingham" }
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "153",
|
|
12
|
+
"name": { "text": "London Leicester Square" },
|
|
13
|
+
"contactDetails": {
|
|
14
|
+
"address": { "line1": "2 Example Square", "line2": "", "city": "London" }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "824",
|
|
19
|
+
"name": { "text": "Brighton" },
|
|
20
|
+
"contactDetails": {
|
|
21
|
+
"address": { "line1": "3 Example Parade", "line2": "Seafront", "city": "Brighton" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "955",
|
|
26
|
+
"name": { "text": "Trafford Centre" },
|
|
27
|
+
"contactDetails": {
|
|
28
|
+
"address": { "line1": "4 Example Dome", "line2": "", "city": "Manchester" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require_relative '../../test_helper'
|
|
2
|
+
|
|
3
|
+
class OdeonUk::CinemaTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@api_response = FakeApiResponse.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_new_returns_cinema_with_string_id
|
|
9
|
+
cinema = OdeonUk::Cinema.new(824)
|
|
10
|
+
|
|
11
|
+
assert_instance_of OdeonUk::Cinema, cinema
|
|
12
|
+
assert_equal '824', cinema.id
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_new_zero_pads_id
|
|
16
|
+
assert_equal '017', OdeonUk::Cinema.new(17).id
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_all_returns_cinemas
|
|
20
|
+
with_fake_api do
|
|
21
|
+
cinemas = OdeonUk::Cinema.all
|
|
22
|
+
|
|
23
|
+
assert(cinemas.all? { |cinema| cinema.instance_of?(OdeonUk::Cinema) })
|
|
24
|
+
assert_equal %w(017 153 824 955), cinemas.map(&:id)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_adr_has_no_postcode
|
|
29
|
+
expected = {
|
|
30
|
+
street_address: '3 Example Parade',
|
|
31
|
+
extended_address: 'Seafront',
|
|
32
|
+
locality: 'Brighton',
|
|
33
|
+
region: nil,
|
|
34
|
+
postal_code: nil,
|
|
35
|
+
country_name: 'United Kingdom'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
with_fake_api do
|
|
39
|
+
assert_equal expected, OdeonUk::Cinema.new(824).adr
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_address_for_london
|
|
44
|
+
expected = {
|
|
45
|
+
street_address: '2 Example Square',
|
|
46
|
+
extended_address: '',
|
|
47
|
+
locality: 'London',
|
|
48
|
+
region: nil,
|
|
49
|
+
postal_code: nil,
|
|
50
|
+
country_name: 'United Kingdom'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
with_fake_api do
|
|
54
|
+
assert_equal expected, OdeonUk::Cinema.new(153).address
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_brand
|
|
59
|
+
assert_equal 'Odeon', OdeonUk::Cinema.new(824).brand
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_full_name
|
|
63
|
+
with_fake_api do
|
|
64
|
+
assert_equal 'Odeon Brighton', OdeonUk::Cinema.new(824).full_name
|
|
65
|
+
assert_equal 'Odeon Leicester Square', OdeonUk::Cinema.new(153).full_name
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_name_removes_london_prefix
|
|
70
|
+
with_fake_api do
|
|
71
|
+
assert_equal 'Leicester Square', OdeonUk::Cinema.new(153).name
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_postal_code_is_empty
|
|
76
|
+
with_fake_api do
|
|
77
|
+
assert_equal '', OdeonUk::Cinema.new(824).postal_code
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_slug
|
|
82
|
+
with_fake_api do
|
|
83
|
+
assert_equal 'odeon-brighton', OdeonUk::Cinema.new(824).slug
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_url
|
|
88
|
+
with_fake_api do
|
|
89
|
+
assert_equal 'https://www.odeon.co.uk/cinemas/brighton/',
|
|
90
|
+
OdeonUk::Cinema.new(824).url
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_url_keeps_london_prefix
|
|
95
|
+
with_fake_api do
|
|
96
|
+
assert_equal 'https://www.odeon.co.uk/cinemas/london-leicester-square/',
|
|
97
|
+
OdeonUk::Cinema.new(153).url
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_url_uses_slug_that_differs_from_name
|
|
102
|
+
with_fake_api do
|
|
103
|
+
assert_equal 'https://www.odeon.co.uk/cinemas/manchester-trafford-centre/',
|
|
104
|
+
OdeonUk::Cinema.new(955).url
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def with_fake_api(&block)
|
|
111
|
+
OdeonUk::Internal::ApiResponse.stub(:new, @api_response, &block)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class OdeonUk::Internal::ApiResponseTest < Minitest::Test
|
|
4
|
+
include ApiFixturesHelper
|
|
5
|
+
|
|
6
|
+
BASE_URL = 'https://vwc.odeon.co.uk/WSVistaWebClient/ocapi/v1'.freeze
|
|
7
|
+
HEADERS = {
|
|
8
|
+
'Authorization' => 'Bearer guest-jwt', 'Connect-Region-Code' => 'UK'
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def setup
|
|
12
|
+
OdeonUk.token = 'guest-jwt'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def teardown
|
|
16
|
+
OdeonUk.token = nil
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_sites
|
|
21
|
+
stub_request(:get, "#{BASE_URL}/sites")
|
|
22
|
+
.with(headers: HEADERS)
|
|
23
|
+
.to_return(status: 200, body: sites_json)
|
|
24
|
+
|
|
25
|
+
sites = api.sites
|
|
26
|
+
|
|
27
|
+
assert_kind_of Array, sites
|
|
28
|
+
assert_includes sites.first.keys, 'id'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_showtimes
|
|
32
|
+
stub_request(:get, "#{BASE_URL}/showtimes/by-business-date/2026-10-24")
|
|
33
|
+
.with(query: { 'siteIds' => '824' }, headers: HEADERS)
|
|
34
|
+
.to_return(status: 200, body: showtimes_json('824'))
|
|
35
|
+
|
|
36
|
+
showtimes = api.showtimes('824', Date.new(2026, 10, 24))
|
|
37
|
+
|
|
38
|
+
assert_equal %w(businessDate relatedData showtimes), showtimes.keys.sort
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_missing_token_raises_before_request
|
|
42
|
+
OdeonUk.token = nil
|
|
43
|
+
|
|
44
|
+
assert_raises(OdeonUk::AuthenticationError) { api.sites }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_rejected_token_raises
|
|
48
|
+
stub_request(:get, "#{BASE_URL}/sites").to_return(status: 401)
|
|
49
|
+
|
|
50
|
+
assert_raises(OdeonUk::AuthenticationError) { api.sites }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_server_error_omits_body
|
|
54
|
+
stub_request(:get, "#{BASE_URL}/sites").to_return(status: 500, body: 'secret')
|
|
55
|
+
|
|
56
|
+
error = assert_raises(RuntimeError) { api.sites }
|
|
57
|
+
|
|
58
|
+
assert_equal 'Vista HTTP error 500', error.message
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def api
|
|
64
|
+
OdeonUk::Internal::ApiResponse.new
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class FixtureShapeTest < Minitest::Test
|
|
4
|
+
include ApiFixturesHelper
|
|
5
|
+
include ApiShapes
|
|
6
|
+
|
|
7
|
+
def test_sites_fixture_matches_shape
|
|
8
|
+
assert_sites_shape(JSON.parse(sites_json))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_showtimes_fixture_matches_shape
|
|
12
|
+
assert_showtimes_shape(JSON.parse(showtimes_json('824')))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
require 'base64'
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
|
|
5
|
+
class OdeonUk::Internal::GuestTokenTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@directory = Dir.mktmpdir('odeon-guest-token')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def teardown
|
|
11
|
+
FileUtils.remove_entry(@directory)
|
|
12
|
+
OdeonUk.token = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_fetch_returns_token_from_page_data
|
|
16
|
+
token = token_expiring_at(Time.now + 3600)
|
|
17
|
+
curl = fake_curl("#{page_with(token)}\n200")
|
|
18
|
+
|
|
19
|
+
assert_equal token, guest_token(curl).fetch
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_fetch_rejects_expired_token
|
|
23
|
+
curl = fake_curl("#{page_with(token_expiring_at(Time.now - 60))}\n200")
|
|
24
|
+
|
|
25
|
+
assert_raises(OdeonUk::AuthenticationError) { guest_token(curl).fetch }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_fetch_rejects_page_without_page_data
|
|
29
|
+
curl = fake_curl("<html></html>\n200")
|
|
30
|
+
|
|
31
|
+
assert_raises(OdeonUk::AuthenticationError) { guest_token(curl).fetch }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_fetch_rejects_redirect_to_queue
|
|
35
|
+
curl = fake_curl("\n302")
|
|
36
|
+
|
|
37
|
+
assert_raises(OdeonUk::AuthenticationError) { guest_token(curl).fetch }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_fetch_rejects_failed_command
|
|
41
|
+
curl = fake_curl('', exit_status: 7)
|
|
42
|
+
|
|
43
|
+
assert_raises(OdeonUk::AuthenticationError) { guest_token(curl).fetch }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_fetch_explains_missing_executable
|
|
47
|
+
error = assert_raises(OdeonUk::AuthenticationError) do
|
|
48
|
+
guest_token(File.join(@directory, 'missing')).fetch
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
assert_match(/curl-impersonate/, error.message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_acquire_token_sets_token
|
|
55
|
+
token = token_expiring_at(Time.now + 3600)
|
|
56
|
+
|
|
57
|
+
OdeonUk.acquire_token!(curl: fake_curl("#{page_with(token)}\n200"))
|
|
58
|
+
|
|
59
|
+
assert_equal token, OdeonUk.token
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def guest_token(curl)
|
|
65
|
+
OdeonUk::Internal::GuestToken.new(curl: curl)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def token_expiring_at(time)
|
|
69
|
+
payload = Base64.urlsafe_encode64(JSON.generate('exp' => time.to_i), padding: false)
|
|
70
|
+
"header.#{payload}.signature"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def page_with(token)
|
|
74
|
+
data = { 'api' => { 'apiUrl' => 'https://vwc.odeon.co.uk/WSVistaWebClient', 'authToken' => token } }
|
|
75
|
+
"<script>\n window.initialData = #{JSON.generate(data)};\n window.pageData = [];\n</script>"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def fake_curl(output, exit_status: 0)
|
|
79
|
+
path = File.join(@directory, 'curl-impersonate')
|
|
80
|
+
File.write(path, "#!#{RbConfig.ruby}\nSTDOUT.write(#{output.dump})\nexit #{exit_status}\n")
|
|
81
|
+
File.chmod(0o755, path)
|
|
82
|
+
path
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require_relative '../../../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class OdeonUk::Internal::Parser::Api::ShowtimeTest < Minitest::Test
|
|
4
|
+
FILMS = {
|
|
5
|
+
'HO1' => { 'title' => { 'text' => 'Jailer 2 (Tamil)' } },
|
|
6
|
+
'HO2' => { 'title' => { 'text' => 'Cine-Excess 2026: Scum Of The Earth' } }
|
|
7
|
+
}.freeze
|
|
8
|
+
|
|
9
|
+
ATTRIBUTES = {
|
|
10
|
+
'1' => { 'shortName' => { 'text' => 'TamilA' }, 'name' => { 'text' => 'Tamil (Audio)' } },
|
|
11
|
+
'2' => { 'shortName' => { 'text' => 'EnglishSub' }, 'name' => { 'text' => 'English (Sub)' } },
|
|
12
|
+
'3' => { 'shortName' => { 'text' => 'IMAX' }, 'name' => { 'text' => 'IMAX' } },
|
|
13
|
+
'4' => { 'shortName' => { 'text' => 'OpenCapt' }, 'name' => { 'text' => 'Open Captioned' } },
|
|
14
|
+
'5' => { 'shortName' => { 'text' => 'Wheelchair' }, 'name' => { 'text' => 'Wheelchair Accessible' } }
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def test_to_hash
|
|
18
|
+
expected = {
|
|
19
|
+
booking_url: 'https://www.odeon.co.uk/ticketing/seat-picker/?showtimeId=824-1',
|
|
20
|
+
dimension: '2d',
|
|
21
|
+
festival: nil,
|
|
22
|
+
film_name: 'Jailer 2',
|
|
23
|
+
language: nil,
|
|
24
|
+
starting_at: Time.utc(2026, 10, 24, 17, 30),
|
|
25
|
+
variant: []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
assert_equal expected, parse
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_3d_glasses
|
|
32
|
+
assert_equal '3d', parse(requires3d: true)[:dimension]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_audio_language
|
|
36
|
+
assert_equal 'Tamil', parse(attribute_ids: %w(1 2 3 5))[:language]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_maps_known_attributes_to_variants
|
|
40
|
+
assert_equal %w(subtitled imax), parse(attribute_ids: %w(1 2 3 5))[:variant]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_festival_moves_from_title_to_attribute
|
|
44
|
+
showtime = parse(film_id: 'HO2')
|
|
45
|
+
|
|
46
|
+
assert_equal Cinebase::Festival.new(name: 'Cine-Excess', year: 2026), showtime[:festival]
|
|
47
|
+
assert_equal 'Scum Of The Earth', showtime[:film_name]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_open_captions_and_subtitles_are_one_variant
|
|
51
|
+
assert_equal %w(subtitled), parse(attribute_ids: %w(2 4))[:variant]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def parse(attribute_ids: [], requires3d: false, film_id: 'HO1')
|
|
57
|
+
data = {
|
|
58
|
+
'id' => '824-1',
|
|
59
|
+
'filmId' => film_id,
|
|
60
|
+
'schedule' => { 'startsAt' => '2026-10-24T18:30:00+01:00' },
|
|
61
|
+
'attributeIds' => attribute_ids,
|
|
62
|
+
'requires3dGlasses' => requires3d
|
|
63
|
+
}
|
|
64
|
+
OdeonUk::Internal::Parser::Api::Showtime.new(
|
|
65
|
+
data, films: FILMS, attributes: ATTRIBUTES
|
|
66
|
+
).to_hash
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class OdeonUk::Internal::TitleSanitizerTest < Minitest::Test
|
|
4
|
+
def test_removes_2d
|
|
5
|
+
assert_sanitized 'Iron Man 3', 'Iron Man 3 2D'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_removes_3d
|
|
9
|
+
assert_sanitized 'Iron Man 3', 'Iron Man 3 3D'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_removes_autism_prefix
|
|
13
|
+
assert_sanitized 'Planes', 'Autism Friendly Screening - Planes'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_bolshoi
|
|
17
|
+
assert_sanitized 'Bolshoi: Spartacus', 'Bolshoi - Spartacus (Live)'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_removes_cinemagic_prefix
|
|
21
|
+
assert_sanitized 'Echo Planet', 'Cinemagic 2013 - Echo Planet'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_globe_on_screen
|
|
25
|
+
assert_sanitized 'Globe: Twelfth Night', 'Globe On Screen: Twelfth Night'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_met_opera
|
|
29
|
+
assert_sanitized 'Met Opera: Eugene Onegin 2013', 'Met Opera - Eugene Onegin 2013'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_met_opera_season
|
|
33
|
+
assert_sanitized 'Met Opera: La Fanciulla del West',
|
|
34
|
+
'Met Opera 2026-27: La Fanciulla del West'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_nt_live
|
|
38
|
+
assert_sanitized 'National Theatre: War Horse', 'NT Live - War Horse'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_national_theatre_encore
|
|
42
|
+
assert_sanitized 'National Theatre: Frankenstein',
|
|
43
|
+
'National Theatre Live - Frankenstein (Encore Screening)'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_roh
|
|
47
|
+
assert_sanitized 'Royal Opera House: Carmen', 'ROH - Carmen'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_rbo_season
|
|
51
|
+
assert_sanitized 'Royal Ballet and Opera: Swan Lake',
|
|
52
|
+
'RBO Cinema Season 2026-27: Swan Lake'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_short_rbo_season
|
|
56
|
+
assert_sanitized "Royal Ballet and Opera: Alice's Adventures in Wonderland",
|
|
57
|
+
"RBO 2026-27: Alice's Adventures in Wonderland"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_rsc
|
|
61
|
+
assert_sanitized 'Royal Shakespeare Company: Richard II', 'Richard II - RSC Live 2013'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_removes_ukjff_prefix
|
|
65
|
+
assert_sanitized 'From Cable Street To Brick Lane',
|
|
66
|
+
'UKJFF - From Cable Street To Brick Lane'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_removes_singalong
|
|
70
|
+
assert_sanitized 'Frozen', 'Frozen Sing-a-long'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_html_ampersands
|
|
74
|
+
assert_sanitized 'Fast & Furious 7', 'Fast & Furious 7'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_removes_language_suffix
|
|
78
|
+
assert_sanitized 'Jailer 2', 'Jailer 2 (Telugu)'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_removes_encore_suffix
|
|
82
|
+
assert_sanitized 'Avengers Endgame', 'Avengers Endgame: Encore'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_keeps_sing_along_within_title
|
|
86
|
+
assert_sanitized 'Bebefinn Sing-Along Movie', 'Bebefinn Sing-Along Movie'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_removes_dubbed
|
|
90
|
+
assert_sanitized 'Arrietty', 'Arrietty (Dubbed)'
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_removes_subbed
|
|
94
|
+
assert_sanitized 'Arrietty', 'Arrietty (Subbed)'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_removes_english_dub
|
|
98
|
+
assert_sanitized 'Kung Fu Soccer', 'Kung Fu Soccer (English Dub)'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_removes_subtitle_description
|
|
102
|
+
assert_sanitized 'Kung Fu Soccer', 'Kung Fu Soccer (Mandarin with EN/CH subtitles)'
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_removes_q_and_a
|
|
106
|
+
assert_sanitized 'NAZA', 'NAZA + Q&A'
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_removes_in_person_q_and_a
|
|
110
|
+
assert_sanitized "Charlie Shaw's Revenge", "Charlie Shaw's Revenge + In-Person Q&A"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_removes_festival_prefix
|
|
114
|
+
assert_sanitized 'Scum Of The Earth', 'Cine-Excess 2026: Scum Of The Earth'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_removes_anniversary
|
|
118
|
+
assert_sanitized 'SCREAM', 'SCREAM (30th Anniversary)'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_world_tour_is_not_capitalised
|
|
122
|
+
assert_sanitized "ENHYPEN World Tour 'BLOOD SAGA'", "ENHYPEN WORLD TOUR 'BLOOD SAGA'"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_keeps_titles_with_their_own_subtitles
|
|
126
|
+
[
|
|
127
|
+
'Harry Potter and the Deathly Hallows: Part 1',
|
|
128
|
+
'Spider-Man: Brand New Day',
|
|
129
|
+
'Hadestown: The Musical',
|
|
130
|
+
'Rammstein - Live in Mexico City',
|
|
131
|
+
'Ozzy & Black Sabbath: Back to the Beginning',
|
|
132
|
+
"BTS 'ARIRANG' IN BUENOS AIRES: LIVE VIEWING"
|
|
133
|
+
].each { |title| assert_sanitized title, title }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def assert_sanitized(expected, title)
|
|
139
|
+
assert_equal expected, OdeonUk::Internal::TitleSanitizer.new(title).sanitized
|
|
140
|
+
end
|
|
141
|
+
end
|