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.
- checksums.yaml +5 -5
- data/.github/workflows/release.yml +73 -0
- data/.github/workflows/test.yml +43 -0
- data/.gitignore +1 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +36 -0
- data/README.md +100 -21
- data/Rakefile +14 -15
- data/lib/odeon_uk/cinema.rb +40 -22
- data/lib/odeon_uk/internal/api_response.rb +19 -39
- data/lib/odeon_uk/internal/guest_token.rb +23 -0
- data/lib/odeon_uk/internal/parser/api/showtime.rb +81 -0
- data/lib/odeon_uk/internal/title_sanitizer.rb +3 -8
- data/lib/odeon_uk/performance.rb +34 -41
- data/lib/odeon_uk/version.rb +3 -3
- data/lib/odeon_uk.rb +16 -2
- data/odeon_uk.gemspec +10 -6
- 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 +66 -215
- data/test/lib/odeon_uk/internal/api_response_test.rb +48 -29
- 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 +77 -0
- data/test/lib/odeon_uk/internal/title_sanitizer_test.rb +109 -89
- data/test/lib/odeon_uk/performance_test.rb +129 -146
- data/test/lib/odeon_uk/version_test.rb +3 -3
- 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 +5 -30
- data/test/support/api_shapes.rb +46 -0
- data/test/support/fake_api_response.rb +9 -6
- data/test/test_helper.rb +6 -3
- metadata +67 -89
- data/.travis.yml +0 -7
- data/lib/odeon_uk/internal/parser/api/film_lookup.rb +0 -34
- data/lib/odeon_uk/internal/parser/api/performance_day.rb +0 -92
- data/rake/fixture_creator.rb +0 -46
- data/test/fixtures/api/all_cinemas.plist +0 -0
- data/test/fixtures/api/app_init.plist +0 -0
- data/test/fixtures/api/film_times/71-14876.plist +0 -0
- data/test/fixtures/api/film_times/71-15408.plist +0 -0
- data/test/fixtures/api/film_times/71-15684.plist +0 -0
- data/test/fixtures/api/film_times/71-15866.plist +0 -0
- data/test/fixtures/api/film_times/71-15868.plist +0 -0
- data/test/fixtures/api/film_times/71-15961.plist +0 -0
- data/test/fixtures/api/film_times/71-15963.plist +0 -0
- data/test/fixtures/api/film_times/71-15965.plist +0 -0
- data/test/fixtures/api/film_times/71-15967.plist +0 -0
- data/test/fixtures/api/film_times/71-15969.plist +0 -0
- data/test/fixtures/api/film_times/71-15977.plist +0 -0
- data/test/fixtures/api/film_times/71-15980.plist +0 -0
- data/test/fixtures/api/film_times/71-16037.plist +0 -0
- data/test/fixtures/api/film_times/71-16136.plist +0 -0
- data/test/fixtures/api/film_times/71-16142.plist +0 -0
- data/test/fixtures/api/film_times/71-16143.plist +0 -0
- data/test/fixtures/api/film_times/71-16187.plist +0 -0
- data/test/fixtures/api/film_times/71-16369.plist +0 -0
- data/test/fixtures/api/film_times/71-16370.plist +0 -0
- data/test/fixtures/api/film_times/71-16374.plist +0 -0
- data/test/fixtures/api/film_times/71-16386.plist +0 -0
- data/test/fixtures/api/film_times/71-16387.plist +0 -0
- data/test/fixtures/api/film_times/71-16388.plist +0 -0
- data/test/fixtures/api/film_times/71-16438.plist +0 -0
- data/test/fixtures/api/film_times/71-16454.plist +0 -0
- data/test/fixtures/api/film_times/71-16457.plist +0 -0
- data/test/fixtures/api/film_times/71-16477.plist +0 -0
- data/test/fixtures/api/film_times/71-16478.plist +0 -0
- data/test/fixtures/api/film_times/71-16480.plist +0 -0
- data/test/fixtures/api/film_times/71-16482.plist +0 -0
- data/test/fixtures/api/film_times/71-16489.plist +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
film_year: Cinebase::TitleSanitizer.film_year(title),
|
|
44
|
+
language: language,
|
|
45
|
+
starting_at: starting_at,
|
|
46
|
+
variant: variant
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def attribute_names(key)
|
|
53
|
+
@data.fetch("attributeIds", []).filter_map do |id|
|
|
54
|
+
@attributes.dig(id, key, "text")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def starting_at
|
|
59
|
+
Time.iso8601(@data.dig("schedule", "startsAt")).utc
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def film_name
|
|
63
|
+
TitleSanitizer.new(title).sanitized
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def title
|
|
67
|
+
@films.fetch(@data.fetch("filmId")).dig("title", "text")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def language
|
|
71
|
+
attribute_names("name").filter_map { |name| name[/\A(.+) \(Audio\)\z/, 1] }.first
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def variant
|
|
75
|
+
attribute_names("shortName").filter_map { |name| VARIANTS[name] }.uniq
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
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
|
-
'
|
|
27
|
-
|
|
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
|
data/lib/odeon_uk/performance.rb
CHANGED
|
@@ -20,19 +20,25 @@ module OdeonUk
|
|
|
20
20
|
# @option options [String] :dimension ('2d') dimension of the screening
|
|
21
21
|
# @option options [String] :film_name name of the film
|
|
22
22
|
# @option options [Time] :starting_at listed start time of the performance
|
|
23
|
+
# @option options [Cinebase::Festival] :festival (nil) festival edition
|
|
24
|
+
# @option options [String] :language (nil) audio language of the screening
|
|
23
25
|
|
|
24
|
-
#
|
|
25
|
-
# @param [Integer] cinema_id id of the cinema on the website
|
|
26
|
+
# Performances within an inclusive range of cinema business dates.
|
|
27
|
+
# @param [Integer, String] cinema_id id of the cinema on the website
|
|
28
|
+
# @param [Date] from first date (defaults to today in London)
|
|
29
|
+
# @param [Date] to last date (defaults to 13 days after from)
|
|
26
30
|
# @return [Array<OdeonUk::Performance>]
|
|
27
|
-
def self.at(
|
|
28
|
-
cinema_id
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
def self.at(
|
|
32
|
+
cinema_id,
|
|
33
|
+
from: TZInfo::Timezone.get('Europe/London').now.to_date,
|
|
34
|
+
to: from + 13
|
|
35
|
+
)
|
|
36
|
+
raise ArgumentError, 'to must not precede from' if to < from
|
|
37
|
+
|
|
38
|
+
api = OdeonUk::Internal::ApiResponse.new
|
|
39
|
+
cinema = Cinema.new(cinema_id)
|
|
40
|
+
(from..to).flat_map do |date|
|
|
41
|
+
performances_on(api, cinema, date)
|
|
36
42
|
end
|
|
37
43
|
end
|
|
38
44
|
|
|
@@ -48,41 +54,28 @@ module OdeonUk
|
|
|
48
54
|
# The kinds of screening (IMAX, kids, baby, senior)
|
|
49
55
|
# @return <Array[String]>
|
|
50
56
|
|
|
51
|
-
# private
|
|
52
|
-
|
|
53
|
-
def self.api_response
|
|
54
|
-
@api_response ||= OdeonUk::Internal::ApiResponse.new
|
|
55
|
-
end
|
|
56
|
-
private_class_method :api_response
|
|
57
|
-
|
|
58
|
-
def self.cinema_hash(cinema_id)
|
|
59
|
-
{ cinema_id: cinema_id, cinema_name: Cinema.new(cinema_id).name }
|
|
60
|
-
end
|
|
61
|
-
private_class_method :cinema_hash
|
|
62
|
-
|
|
63
|
-
def self.films_at(cinema_id)
|
|
64
|
-
film_lookup.at(cinema_id)
|
|
65
|
-
end
|
|
66
|
-
private_class_method :films_at
|
|
67
57
|
|
|
68
|
-
|
|
69
|
-
@film_lookup ||= OdeonUk::Internal::Parser::Api::FilmLookup.new
|
|
70
|
-
end
|
|
71
|
-
private_class_method :film_lookup
|
|
58
|
+
# private
|
|
72
59
|
|
|
73
|
-
def self.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
60
|
+
def self.performances_on(api, cinema, date)
|
|
61
|
+
response = api.showtimes(cinema.id, date)
|
|
62
|
+
related = response.fetch('relatedData')
|
|
63
|
+
films = by_id(related.fetch('films'))
|
|
64
|
+
attributes = by_id(related.fetch('attributes'))
|
|
77
65
|
|
|
78
|
-
|
|
79
|
-
|
|
66
|
+
response.fetch('showtimes').map do |data|
|
|
67
|
+
showtime = OdeonUk::Internal::Parser::Api::Showtime.new(
|
|
68
|
+
data, films: films, attributes: attributes
|
|
69
|
+
)
|
|
70
|
+
new(showtime.to_hash.merge(cinema_id: cinema.id,
|
|
71
|
+
cinema_name: cinema.name))
|
|
72
|
+
end
|
|
80
73
|
end
|
|
81
|
-
private_class_method :
|
|
74
|
+
private_class_method :performances_on
|
|
82
75
|
|
|
83
|
-
def self.
|
|
84
|
-
|
|
76
|
+
def self.by_id(items)
|
|
77
|
+
items.to_h { |item| [item.fetch('id'), item] }
|
|
85
78
|
end
|
|
86
|
-
private_class_method :
|
|
79
|
+
private_class_method :by_id
|
|
87
80
|
end
|
|
88
81
|
end
|
data/lib/odeon_uk/version.rb
CHANGED
data/lib/odeon_uk.rb
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
1
3
|
require 'cinebase'
|
|
4
|
+
require 'cinebase/vista'
|
|
2
5
|
|
|
3
6
|
require_relative './odeon_uk/version'
|
|
4
7
|
|
|
5
8
|
require_relative './odeon_uk/internal/api_response'
|
|
9
|
+
require_relative './odeon_uk/internal/guest_token'
|
|
6
10
|
require_relative './odeon_uk/internal/title_sanitizer'
|
|
7
11
|
|
|
8
|
-
require_relative './odeon_uk/internal/parser/api/
|
|
9
|
-
require_relative './odeon_uk/internal/parser/api/performance_day'
|
|
12
|
+
require_relative './odeon_uk/internal/parser/api/showtime'
|
|
10
13
|
|
|
11
14
|
require_relative './odeon_uk/cinema'
|
|
12
15
|
require_relative './odeon_uk/performance'
|
|
13
16
|
|
|
14
17
|
# odeon_uk gem
|
|
15
18
|
module OdeonUk
|
|
19
|
+
AuthenticationError = Cinebase::Vista::AuthenticationError
|
|
20
|
+
|
|
21
|
+
# A raw guest JWT, without the "Bearer " prefix. Never persisted by the gem.
|
|
22
|
+
class << self
|
|
23
|
+
attr_accessor :token
|
|
24
|
+
|
|
25
|
+
# Explicit acquisition/renewal; ordinary API calls never launch a subprocess.
|
|
26
|
+
def acquire_token!(curl: nil)
|
|
27
|
+
self.token = Internal::GuestToken.new(curl: curl).fetch
|
|
28
|
+
end
|
|
29
|
+
end
|
|
16
30
|
end
|
data/odeon_uk.gemspec
CHANGED
|
@@ -7,21 +7,25 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.version = OdeonUk::VERSION
|
|
8
8
|
spec.authors = ['Andy Croll']
|
|
9
9
|
spec.email = ['andy@goodscary.com']
|
|
10
|
-
spec.description = 'Pull movie & cinema information from the
|
|
10
|
+
spec.description = 'Pull movie & cinema information from the ODEON Vista API'
|
|
11
11
|
spec.summary = "It's a scraper, but a nice one"
|
|
12
|
-
spec.homepage = '
|
|
12
|
+
spec.homepage = 'https://github.com/andycroll/odeon_uk'
|
|
13
13
|
spec.licenses = %w(AGPL MIT)
|
|
14
14
|
|
|
15
|
-
spec.
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.files = `git ls-files`.split("\n")
|
|
16
18
|
spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
17
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
18
20
|
spec.require_paths = ['lib']
|
|
19
21
|
|
|
20
|
-
spec.add_development_dependency '
|
|
22
|
+
spec.add_development_dependency 'base64'
|
|
23
|
+
spec.add_development_dependency 'bundler', '>= 2.0'
|
|
24
|
+
spec.add_development_dependency 'minitest-mock'
|
|
21
25
|
spec.add_development_dependency 'minitest-reporters'
|
|
22
26
|
spec.add_development_dependency 'rake'
|
|
27
|
+
spec.add_development_dependency 'simplecov', '< 1'
|
|
23
28
|
spec.add_development_dependency 'webmock'
|
|
24
29
|
|
|
25
|
-
spec.add_runtime_dependency '
|
|
26
|
-
spec.add_runtime_dependency 'cinebase', '~> 3.0'
|
|
30
|
+
spec.add_runtime_dependency 'cinebase', '~> 5.4'
|
|
27
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 (2025) (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
|
+
}
|