odeon_uk 5.1.1-x86_64-linux-gnu → 5.3.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 +4 -4
- data/CHANGELOG.md +20 -0
- data/Gemfile +1 -0
- data/README.md +21 -3
- data/lib/odeon_uk/cinema.rb +27 -0
- data/lib/odeon_uk/internal/api_response.rb +9 -2
- data/lib/odeon_uk/internal/parser/api/showtime.rb +1 -0
- data/lib/odeon_uk/internal/title_sanitizer.rb +3 -1
- data/lib/odeon_uk/version.rb +2 -2
- data/lib/odeon_uk.rb +5 -8
- data/odeon_uk.gemspec +1 -1
- data/test/fixtures/api/sites.json +3 -0
- data/test/lib/odeon_uk/cinema_test.rb +25 -0
- data/test/lib/odeon_uk/internal/api_response_test.rb +8 -0
- data/test/lib/odeon_uk/internal/guest_token_test.rb +7 -0
- data/test/lib/odeon_uk/internal/parser/api/showtime_test.rb +1 -0
- data/test/lib/odeon_uk/internal/title_sanitizer_test.rb +19 -7
- data/test/lib/odeon_uk/performance_test.rb +14 -0
- data/test/live/integration_test.rb +10 -0
- data/test/support/api_shapes.rb +5 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc7575256bde69b11ed9e334e248c60ebe5052505b1774826f5ef2a41af28855
|
|
4
|
+
data.tar.gz: '082b20d79f181f1c8fe4386cde1a8909b4538acaa9041f40f82d0246719abd41'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd71a970f25463f8cfc1d8c8ec380efc46cf7332c19c685edf275a4f108190ce0bf76c1c84f67045c2eb837b63dea00556e1c985225785985fc178642a0ecb66
|
|
7
|
+
data.tar.gz: 5780c617ae27789af681ecb8032ef625431851f31822cbed5a8bf2bc95efa7d183c81f381e80ebe91d7bff8f60271f58cd573c6fee18d6348204ee556914d0b9
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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.3.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 and Bolshoi screenings are named `National Theatre Live: ` and `Bolshoi Ballet: `, the prefixes cinebase moves to `event`. Titles listed in capitals, such as `SCREAM`, are title cased.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `ODEON Pride Nights - A Wolf Among the Swans` is now `A Wolf Among the Swans`.
|
|
13
|
+
- `Charity Screening - Inside Out` is now `Inside Out`.
|
|
14
|
+
|
|
15
|
+
## 5.2.0 - 2026-10-31
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `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.
|
|
19
|
+
- `OdeonUk.ensure_token!` fetches a guest token only when there is none or it expires within an hour, and `OdeonUk.token_expires_at` reads its expiry. Both come from cinebase's `Vista::TokenHolder`.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- A token the API rejects is cleared from `OdeonUk.token`, so the next `ensure_token!` fetches a fresh one.
|
|
23
|
+
- Requires cinebase 5.6.
|
|
24
|
+
|
|
5
25
|
## 5.1.1 - 2026-10-22
|
|
6
26
|
|
|
7
27
|
### Changed
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -57,9 +57,21 @@ OdeonUk.token = ENV.fetch('ODEON_TOKEN')
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Tokens currently last 12 hours. The gem checks the declared expiry but does not
|
|
60
|
-
verify the signature.
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
verify the signature. Normal API calls never acquire or renew a token
|
|
61
|
+
automatically. `OdeonUk.ensure_token!` fetches one only when there is no token,
|
|
62
|
+
or the token expires within an hour, and otherwise keeps the current one. Call
|
|
63
|
+
it before each batch of requests:
|
|
64
|
+
|
|
65
|
+
``` ruby
|
|
66
|
+
OdeonUk.ensure_token!
|
|
67
|
+
OdeonUk.token_expires_at
|
|
68
|
+
#=> 2026-10-31 03:00:00 UTC
|
|
69
|
+
OdeonUk::Cinema.all
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Missing, expired or rejected tokens raise `OdeonUk::AuthenticationError`. A
|
|
73
|
+
token the API rejects is cleared from `OdeonUk.token`, so the next
|
|
74
|
+
`ensure_token!` fetches a fresh one.
|
|
63
75
|
|
|
64
76
|
### Cinema
|
|
65
77
|
|
|
@@ -93,6 +105,12 @@ cinema.full_name
|
|
|
93
105
|
cinema.id
|
|
94
106
|
#=> '824'
|
|
95
107
|
|
|
108
|
+
cinema.latitude
|
|
109
|
+
#=> 50.821 # Float, nil when the API has no location
|
|
110
|
+
|
|
111
|
+
cinema.longitude
|
|
112
|
+
#=> -0.1452
|
|
113
|
+
|
|
96
114
|
cinema.url
|
|
97
115
|
#=> 'https://www.odeon.co.uk/cinemas/brighton/'
|
|
98
116
|
```
|
data/lib/odeon_uk/cinema.rb
CHANGED
|
@@ -105,6 +105,26 @@ module OdeonUk
|
|
|
105
105
|
# cinema.locality
|
|
106
106
|
# #=> 'Brighton'
|
|
107
107
|
|
|
108
|
+
# Latitude of the cinema
|
|
109
|
+
# @return [Float, nil] decimal degrees, or nil when unknown
|
|
110
|
+
# @example
|
|
111
|
+
# cinema = OdeonUk::Cinema.new(824)
|
|
112
|
+
# cinema.latitude
|
|
113
|
+
# #=> 50.821
|
|
114
|
+
def latitude
|
|
115
|
+
coordinates&.first
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Longitude of the cinema
|
|
119
|
+
# @return [Float, nil] decimal degrees, or nil when unknown
|
|
120
|
+
# @example
|
|
121
|
+
# cinema = OdeonUk::Cinema.new(824)
|
|
122
|
+
# cinema.longitude
|
|
123
|
+
# #=> -0.1452
|
|
124
|
+
def longitude
|
|
125
|
+
coordinates&.last
|
|
126
|
+
end
|
|
127
|
+
|
|
108
128
|
# The name of the cinema
|
|
109
129
|
# @return [String]
|
|
110
130
|
# @example
|
|
@@ -164,6 +184,13 @@ module OdeonUk
|
|
|
164
184
|
|
|
165
185
|
private
|
|
166
186
|
|
|
187
|
+
# 0, 0 marks a site whose position was never set
|
|
188
|
+
def coordinates
|
|
189
|
+
location = site_hash['location'] || {}
|
|
190
|
+
pair = [location['latitude'], location['longitude']]
|
|
191
|
+
pair.map(&:to_f) if pair.all?(Numeric) && pair.any?(&:nonzero?)
|
|
192
|
+
end
|
|
193
|
+
|
|
167
194
|
def site_hash
|
|
168
195
|
@site_hash ||= self.class.send(:sites_hash).fetch(id, {})
|
|
169
196
|
end
|
|
@@ -14,19 +14,26 @@ module OdeonUk
|
|
|
14
14
|
|
|
15
15
|
# @return [Array<Hash>] UK cinemas
|
|
16
16
|
def sites
|
|
17
|
-
|
|
17
|
+
get("sites").fetch("sites")
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# @param [String] cinema_id zero-padded site id, e.g. '017'
|
|
21
21
|
# @param [Date] date business date
|
|
22
22
|
# @return [Hash] showtimes plus their related films and attributes
|
|
23
23
|
def showtimes(cinema_id, date)
|
|
24
|
-
|
|
24
|
+
get("showtimes/by-business-date/#{date.iso8601}",
|
|
25
25
|
"siteIds" => cinema_id)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
private
|
|
29
29
|
|
|
30
|
+
def get(path, query = {})
|
|
31
|
+
client.get(path, query)
|
|
32
|
+
rescue Cinebase::Vista::AuthenticationError
|
|
33
|
+
OdeonUk.reject_token!(@client_token)
|
|
34
|
+
raise
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
# One client keeps one connection open, so reuse it until the token changes.
|
|
31
38
|
def client
|
|
32
39
|
token = OdeonUk.token
|
|
@@ -21,7 +21,9 @@ module OdeonUk
|
|
|
21
21
|
'Autism Friendly Screening -', # autism screening
|
|
22
22
|
/\(encore.+\)/i, # encore for NT Live
|
|
23
23
|
/\(live\)/i, # live in brackets
|
|
24
|
-
'Autism Friendly - '
|
|
24
|
+
'Autism Friendly - ', # autism friendly
|
|
25
|
+
/\AODEON Pride Nights\s*-\s*/, # pride season
|
|
26
|
+
/\ACharity Screening\s*-\s*/ # charity screening
|
|
25
27
|
] + COMMON_REMOVE
|
|
26
28
|
end
|
|
27
29
|
|
data/lib/odeon_uk/version.rb
CHANGED
data/lib/odeon_uk.rb
CHANGED
|
@@ -18,13 +18,10 @@ require_relative './odeon_uk/performance'
|
|
|
18
18
|
module OdeonUk
|
|
19
19
|
AuthenticationError = Cinebase::Vista::AuthenticationError
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def acquire_token!(curl: nil)
|
|
27
|
-
self.token = Internal::GuestToken.new(curl: curl).fetch
|
|
28
|
-
end
|
|
21
|
+
extend Cinebase::Vista::TokenHolder
|
|
22
|
+
|
|
23
|
+
# @api private
|
|
24
|
+
def self.guest_token_class
|
|
25
|
+
Internal::GuestToken
|
|
29
26
|
end
|
|
30
27
|
end
|
data/odeon_uk.gemspec
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
{
|
|
4
4
|
"id": "017",
|
|
5
5
|
"name": { "text": "Birmingham New Street" },
|
|
6
|
+
"location": { "latitude": 0, "longitude": 0 },
|
|
6
7
|
"contactDetails": {
|
|
7
8
|
"address": { "line1": "1 Example Arcade", "line2": "New Street", "city": "Birmingham" }
|
|
8
9
|
}
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
{
|
|
11
12
|
"id": "153",
|
|
12
13
|
"name": { "text": "London Leicester Square" },
|
|
14
|
+
"location": null,
|
|
13
15
|
"contactDetails": {
|
|
14
16
|
"address": { "line1": "2 Example Square", "line2": "", "city": "London" }
|
|
15
17
|
}
|
|
@@ -17,6 +19,7 @@
|
|
|
17
19
|
{
|
|
18
20
|
"id": "824",
|
|
19
21
|
"name": { "text": "Brighton" },
|
|
22
|
+
"location": { "latitude": 50.8215, "longitude": -0.146 },
|
|
20
23
|
"contactDetails": {
|
|
21
24
|
"address": { "line1": "3 Example Parade", "line2": "Seafront", "city": "Brighton" }
|
|
22
25
|
}
|
|
@@ -66,6 +66,31 @@ class OdeonUk::CinemaTest < Minitest::Test
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
def test_latitude_and_longitude
|
|
70
|
+
with_fake_api do
|
|
71
|
+
cinema = OdeonUk::Cinema.new(824)
|
|
72
|
+
|
|
73
|
+
assert_in_delta 50.8215, cinema.latitude, 0.0001
|
|
74
|
+
assert_in_delta(-0.146, cinema.longitude, 0.0001)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_latitude_and_longitude_are_nil_without_a_location
|
|
79
|
+
with_fake_api do
|
|
80
|
+
[153, 955].each do |id|
|
|
81
|
+
assert_nil OdeonUk::Cinema.new(id).latitude
|
|
82
|
+
assert_nil OdeonUk::Cinema.new(id).longitude
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_latitude_and_longitude_are_nil_at_zero_zero
|
|
88
|
+
with_fake_api do
|
|
89
|
+
assert_nil OdeonUk::Cinema.new(17).latitude
|
|
90
|
+
assert_nil OdeonUk::Cinema.new(17).longitude
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
69
94
|
def test_name_removes_london_prefix
|
|
70
95
|
with_fake_api do
|
|
71
96
|
assert_equal 'Leicester Square', OdeonUk::Cinema.new(153).name
|
|
@@ -50,6 +50,14 @@ class OdeonUk::Internal::ApiResponseTest < Minitest::Test
|
|
|
50
50
|
assert_raises(OdeonUk::AuthenticationError) { api.sites }
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def test_rejected_token_is_cleared
|
|
54
|
+
stub_request(:get, "#{BASE_URL}/sites").to_return(status: 401)
|
|
55
|
+
|
|
56
|
+
assert_raises(OdeonUk::AuthenticationError) { api.sites }
|
|
57
|
+
|
|
58
|
+
assert_nil OdeonUk.token
|
|
59
|
+
end
|
|
60
|
+
|
|
53
61
|
def test_server_error_omits_body
|
|
54
62
|
stub_request(:get, "#{BASE_URL}/sites").to_return(status: 500, body: 'secret')
|
|
55
63
|
|
|
@@ -51,6 +51,13 @@ class OdeonUk::Internal::GuestTokenTest < Minitest::Test
|
|
|
51
51
|
assert_match(/curl-impersonate/, error.message)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
def test_ensure_token_fetches_only_without_a_current_token
|
|
55
|
+
token = token_expiring_at(Time.now + 7200)
|
|
56
|
+
|
|
57
|
+
assert_equal token, OdeonUk.ensure_token!(curl: fake_curl("#{page_with(token)}\n200"))
|
|
58
|
+
assert_equal token, OdeonUk.ensure_token!(curl: File.join(@directory, 'missing'))
|
|
59
|
+
end
|
|
60
|
+
|
|
54
61
|
def test_acquire_token_sets_token
|
|
55
62
|
token = token_expiring_at(Time.now + 3600)
|
|
56
63
|
|
|
@@ -14,7 +14,7 @@ class OdeonUk::Internal::TitleSanitizerTest < Minitest::Test
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def test_bolshoi
|
|
17
|
-
assert_sanitized 'Bolshoi: Spartacus', 'Bolshoi - Spartacus (Live)'
|
|
17
|
+
assert_sanitized 'Bolshoi Ballet: Spartacus', 'Bolshoi - Spartacus (Live)'
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def test_removes_cinemagic_prefix
|
|
@@ -35,11 +35,11 @@ class OdeonUk::Internal::TitleSanitizerTest < Minitest::Test
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def test_nt_live
|
|
38
|
-
assert_sanitized 'National Theatre: War Horse', 'NT Live - War Horse'
|
|
38
|
+
assert_sanitized 'National Theatre Live: War Horse', 'NT Live - War Horse'
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def test_national_theatre_encore
|
|
42
|
-
assert_sanitized 'National Theatre: Frankenstein',
|
|
42
|
+
assert_sanitized 'National Theatre Live: Frankenstein',
|
|
43
43
|
'National Theatre Live - Frankenstein (Encore Screening)'
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -103,7 +103,7 @@ class OdeonUk::Internal::TitleSanitizerTest < Minitest::Test
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def test_removes_q_and_a
|
|
106
|
-
assert_sanitized '
|
|
106
|
+
assert_sanitized 'Naza', 'NAZA + Q&A'
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def test_removes_in_person_q_and_a
|
|
@@ -115,7 +115,15 @@ class OdeonUk::Internal::TitleSanitizerTest < Minitest::Test
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def test_removes_anniversary
|
|
118
|
-
assert_sanitized '
|
|
118
|
+
assert_sanitized 'Scream', 'SCREAM (30th Anniversary)'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_removes_pride_nights_prefix
|
|
122
|
+
assert_sanitized 'A Wolf Among the Swans', 'ODEON Pride Nights - A Wolf Among the Swans'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_removes_charity_screening_prefix
|
|
126
|
+
assert_sanitized 'Inside Out', 'Charity Screening - Inside Out'
|
|
119
127
|
end
|
|
120
128
|
|
|
121
129
|
def test_world_tour_is_not_capitalised
|
|
@@ -128,11 +136,15 @@ class OdeonUk::Internal::TitleSanitizerTest < Minitest::Test
|
|
|
128
136
|
'Spider-Man: Brand New Day',
|
|
129
137
|
'Hadestown: The Musical',
|
|
130
138
|
'Rammstein - Live in Mexico City',
|
|
131
|
-
'Ozzy & Black Sabbath: Back to the Beginning'
|
|
132
|
-
"BTS 'ARIRANG' IN BUENOS AIRES: LIVE VIEWING"
|
|
139
|
+
'Ozzy & Black Sabbath: Back to the Beginning'
|
|
133
140
|
].each { |title| assert_sanitized title, title }
|
|
134
141
|
end
|
|
135
142
|
|
|
143
|
+
def test_title_cases_capitals
|
|
144
|
+
assert_sanitized "BTS 'Arirang' in Buenos Aires: Live Viewing",
|
|
145
|
+
"BTS 'ARIRANG' IN BUENOS AIRES: LIVE VIEWING"
|
|
146
|
+
end
|
|
147
|
+
|
|
136
148
|
private
|
|
137
149
|
|
|
138
150
|
def assert_sanitized(expected, title)
|
|
@@ -36,6 +36,20 @@ class OdeonUk::PerformanceTest < Minitest::Test
|
|
|
36
36
|
performance.booking_url
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def test_at_names_event_from_title
|
|
40
|
+
@api_response.define_singleton_method(:showtimes) do |cinema_id, date|
|
|
41
|
+
super(cinema_id, date).tap do |data|
|
|
42
|
+
data.dig('relatedData', 'films', 0, 'title')['text'] = 'Met Opera - Eugene Onegin'
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
performance = performances_at(824).first
|
|
46
|
+
|
|
47
|
+
assert_equal 'Met Opera - Eugene Onegin', performance.title
|
|
48
|
+
assert_equal 'Eugene Onegin', performance.film_name
|
|
49
|
+
assert_equal Cinebase::Event.new(name: 'Met Opera'), performance.event
|
|
50
|
+
assert_equal %w(arts audio_described), performance.variant
|
|
51
|
+
end
|
|
52
|
+
|
|
39
53
|
def test_at_parses_3d_imax
|
|
40
54
|
performance = performances_at(824)[1]
|
|
41
55
|
|
|
@@ -8,6 +8,16 @@ class LiveCinemaTest < Minitest::Test
|
|
|
8
8
|
assert_operator cinemas.size, :>, 90
|
|
9
9
|
assert_equal 'Brighton', cinemas.detect { |cinema| cinema.id == '824' }.name
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
def test_cinemas_have_uk_coordinates
|
|
13
|
+
located = OdeonUk::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
|
data/test/support/api_shapes.rb
CHANGED
|
@@ -15,6 +15,11 @@ module ApiShapes
|
|
|
15
15
|
%w(line1 line2 city).each do |key|
|
|
16
16
|
assert_kind_of String, address.fetch(key), "site #{site['id']} #{key}"
|
|
17
17
|
end
|
|
18
|
+
location = site['location'] || {}
|
|
19
|
+
%w(latitude longitude).each do |key|
|
|
20
|
+
value = location[key]
|
|
21
|
+
assert_kind_of Numeric, value, "site #{site['id']} #{key}" unless value.nil?
|
|
22
|
+
end
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
25
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: odeon_uk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.3.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.
|
|
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.
|
|
123
|
+
version: '5.9'
|
|
124
124
|
description: Pull movie & cinema information from the ODEON Vista API
|
|
125
125
|
email:
|
|
126
126
|
- andy@goodscary.com
|