scott_uk 1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/COMM-LICENSE +22 -0
- data/LICENSE +617 -0
- data/README.md +151 -0
- data/lib/scott_uk/cinema.rb +123 -0
- data/lib/scott_uk/internal/feed.rb +24 -0
- data/lib/scott_uk/internal/http.rb +39 -0
- data/lib/scott_uk/internal/parser/showtime.rb +87 -0
- data/lib/scott_uk/internal/title_sanitizer.rb +36 -0
- data/lib/scott_uk/performance.rb +90 -0
- data/lib/scott_uk/version.rb +6 -0
- data/lib/scott_uk.rb +18 -0
- metadata +151 -0
data/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# ScottUk
|
|
2
|
+
|
|
3
|
+
A simple gem to pull cinemas and performances from the [Scott Cinemas website](https://www.scottcinemas.co.uk) and spit out useful formatted info.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
gem 'scott_uk'
|
|
10
|
+
|
|
11
|
+
And then execute:
|
|
12
|
+
|
|
13
|
+
$ bundle
|
|
14
|
+
|
|
15
|
+
Or install it yourself as:
|
|
16
|
+
|
|
17
|
+
$ gem install scott_uk
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
Performance titles are sanitized as much as possible, removing years,
|
|
24
|
+
festival suffixes, the parent and baby suffix and the film society prefix,
|
|
25
|
+
and naming National Theatre Live and Royal Ballet and Opera screenings in
|
|
26
|
+
full.
|
|
27
|
+
|
|
28
|
+
No token or setup is needed. Scott Cinemas publishes a JSON feed of each
|
|
29
|
+
venue's listings, documented at https://www.scottcinemas.co.uk/api. Tickets
|
|
30
|
+
are sold through TapOS, behind each venue's own booking pages.
|
|
31
|
+
|
|
32
|
+
### Cinema
|
|
33
|
+
|
|
34
|
+
The website keeps the chosen cinema in a cookie and has no plain list of
|
|
35
|
+
venues, so the gem holds them: names, addresses and feed codes from the
|
|
36
|
+
website's cinema and API pages, and coordinates from its cinema finder. Cinema
|
|
37
|
+
methods make no requests. The four WTW cinemas in Cornwall, which share the
|
|
38
|
+
website's cinema menu, run a separate site and are not included.
|
|
39
|
+
|
|
40
|
+
``` ruby
|
|
41
|
+
ScottUk::Cinema.all
|
|
42
|
+
#=> [<ScottUk::Cinema ...>, <ScottUk::Cinema ...>, ...]
|
|
43
|
+
|
|
44
|
+
cinema = ScottUk::Cinema.new('SCOBWR')
|
|
45
|
+
#=> <ScottUk::Cinema ...>
|
|
46
|
+
|
|
47
|
+
cinema.adr
|
|
48
|
+
#=> {
|
|
49
|
+
street_address: 'Northgate Yard',
|
|
50
|
+
extended_address: 'Mount Street',
|
|
51
|
+
locality: 'Bridgwater',
|
|
52
|
+
region: 'Somerset',
|
|
53
|
+
postal_code: 'TA6 3FQ',
|
|
54
|
+
country_name: 'United Kingdom'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
cinema.brand
|
|
58
|
+
#=> 'Scott Cinemas'
|
|
59
|
+
|
|
60
|
+
cinema.full_name
|
|
61
|
+
#=> 'Scott Cinemas Bridgwater'
|
|
62
|
+
|
|
63
|
+
cinema.id
|
|
64
|
+
#=> 'SCOBWR'
|
|
65
|
+
|
|
66
|
+
cinema.latitude
|
|
67
|
+
#=> 51.12985
|
|
68
|
+
|
|
69
|
+
cinema.url
|
|
70
|
+
#=> 'https://bridgwater.scottcinemas.co.uk'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Performances
|
|
74
|
+
|
|
75
|
+
`Performance.at` returns performances for an inclusive range of showing dates,
|
|
76
|
+
defaulting to today in London and the following 13 days. The venue's feed
|
|
77
|
+
holds every advertised date, so any range is one request.
|
|
78
|
+
|
|
79
|
+
``` ruby
|
|
80
|
+
ScottUk::Performance.at('SCOBWR', from: Date.today, to: Date.today + 2)
|
|
81
|
+
#=> [<ScottUk::Performance ...>, <ScottUk::Performance ...>, ...]
|
|
82
|
+
|
|
83
|
+
performance = ScottUk::Performance.at('SCOBWR').first
|
|
84
|
+
#=> <ScottUk::Performance ...>
|
|
85
|
+
|
|
86
|
+
performance.film_name
|
|
87
|
+
#=> 'Ghosts: The Possession of Button House' # from '... (Parent and Baby Only Show)'
|
|
88
|
+
|
|
89
|
+
performance.film_year
|
|
90
|
+
#=> nil # from the title, e.g. 1975 for 'Barry Lyndon (1975)'
|
|
91
|
+
|
|
92
|
+
performance.dimension
|
|
93
|
+
#=> '2d'
|
|
94
|
+
|
|
95
|
+
performance.variant
|
|
96
|
+
#=> ['baby']
|
|
97
|
+
|
|
98
|
+
performance.subtitled?
|
|
99
|
+
#=> false
|
|
100
|
+
|
|
101
|
+
performance.festival
|
|
102
|
+
#=> nil # e.g. #<data Cinebase::Festival name="Exmouth Film Festival", year=2026>
|
|
103
|
+
|
|
104
|
+
performance.starting_at
|
|
105
|
+
#=> 2026-11-04 11:00:00 UTC
|
|
106
|
+
|
|
107
|
+
performance.booking_url
|
|
108
|
+
#=> 'https://bridgwater.scottcinemas.co.uk/book-now/ghosts-the-possession-parent-baby/76429'
|
|
109
|
+
|
|
110
|
+
performance.cinema_name
|
|
111
|
+
#=> 'Bridgwater'
|
|
112
|
+
|
|
113
|
+
performance.cinema_id
|
|
114
|
+
#=> 'SCOBWR'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Booking links are the feed's own, to the venue's booking page, which holds
|
|
118
|
+
the TapOS seat selection.
|
|
119
|
+
|
|
120
|
+
Variants come from the feed's flags and fields: `arts` (event cinema:
|
|
121
|
+
theatre, opera, ballet, musicals, music, art and television), `baby`,
|
|
122
|
+
`relaxed` (autism friendly), `senior` (Silver Screen) and `subtitled`. Most
|
|
123
|
+
screenings offer audio description, so it is not a variant. Strobe warnings,
|
|
124
|
+
ticket offers and premium screens are not variants either.
|
|
125
|
+
|
|
126
|
+
## Tests
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
bundle exec rake # unit tests against fixtures
|
|
130
|
+
bundle exec rake live # live scottcinemas.co.uk
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The fixture in `test/fixtures` is Bridgwater's feed, trimmed to a few dates
|
|
134
|
+
with shortened descriptions. `test/support/api_shapes.rb` describes the fields
|
|
135
|
+
the gem reads. The unit suite checks the fixture against it and the live suite
|
|
136
|
+
checks every venue's real feed, held only in memory. When the feed changes,
|
|
137
|
+
update the shape first, then the fixture.
|
|
138
|
+
|
|
139
|
+
## Releasing
|
|
140
|
+
|
|
141
|
+
Bump the version, merge, then run the **Release Gem** workflow in GitHub
|
|
142
|
+
Actions. It runs the tests, tags the version and publishes the gem through
|
|
143
|
+
RubyGems trusted publishing.
|
|
144
|
+
|
|
145
|
+
## Contributing
|
|
146
|
+
|
|
147
|
+
1. Fork it
|
|
148
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
149
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
150
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
151
|
+
5. Create new Pull Request
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module ScottUk
|
|
2
|
+
# The object representing a cinema on the Scott Cinemas website
|
|
3
|
+
class Cinema < Cinebase::Cinema
|
|
4
|
+
# @!attribute [r] id
|
|
5
|
+
# @return [String] the cinema's feed code, e.g. 'SCOBAR'
|
|
6
|
+
|
|
7
|
+
# The website keeps the chosen cinema in a cookie and has no plain venue
|
|
8
|
+
# list, so the gem holds the venues: names, addresses and feed codes
|
|
9
|
+
# from the website's cinema and API pages, coordinates from its cinema
|
|
10
|
+
# finder.
|
|
11
|
+
VENUES = {
|
|
12
|
+
'SCOBAR' => {
|
|
13
|
+
name: 'Barnstaple', subdomain: 'barnstaple', street_address: 'Boutport Street', locality: 'Barnstaple',
|
|
14
|
+
region: 'Devon', postal_code: 'EX31 1SR', latitude: 51.07933, longitude: -4.05859
|
|
15
|
+
},
|
|
16
|
+
'SCOBWR' => {
|
|
17
|
+
name: 'Bridgwater', subdomain: 'bridgwater', street_address: 'Northgate Yard',
|
|
18
|
+
extended_address: 'Mount Street', locality: 'Bridgwater', region: 'Somerset', postal_code: 'TA6 3FQ',
|
|
19
|
+
latitude: 51.12985, longitude: -3.006949
|
|
20
|
+
},
|
|
21
|
+
'SCOORP' => {
|
|
22
|
+
name: 'Bristol, Westbury Park', subdomain: 'bristolwestburypark', street_address: 'Northumbria Drive',
|
|
23
|
+
extended_address: 'Westbury Park', locality: 'Bristol', postal_code: 'BS9 4HN',
|
|
24
|
+
latitude: 51.480392, longitude: -2.612085
|
|
25
|
+
},
|
|
26
|
+
'SCOEXM' => {
|
|
27
|
+
name: 'Exmouth', subdomain: 'exmouth', street_address: 'Strand Gardens', locality: 'Exmouth',
|
|
28
|
+
region: 'Devon', postal_code: 'EX8 1HP', latitude: 50.619789, longitude: -3.413336
|
|
29
|
+
},
|
|
30
|
+
'SCONEW' => {
|
|
31
|
+
name: 'Newton Abbot', subdomain: 'newtonabbot', street_address: 'Market Street', locality: 'Newton Abbot',
|
|
32
|
+
region: 'Devon', postal_code: 'TQ12 2RB', latitude: 50.530781, longitude: -3.61197
|
|
33
|
+
},
|
|
34
|
+
'SCORAD' => {
|
|
35
|
+
name: 'Sidmouth', subdomain: 'sidmouth', street_address: 'Radway Place', locality: 'Sidmouth',
|
|
36
|
+
region: 'Devon', postal_code: 'EX10 8TL', latitude: 50.682728, longitude: -3.238649
|
|
37
|
+
}
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
ADDRESS_PARTS = %i(street_address extended_address locality region postal_code).freeze
|
|
41
|
+
|
|
42
|
+
# Constructor
|
|
43
|
+
# @param [String] id the cinema's feed code, e.g. 'SCOBAR' or 'scobar'
|
|
44
|
+
# @return [ScottUk::Cinema]
|
|
45
|
+
def initialize(id)
|
|
46
|
+
@id = id.to_s.upcase
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Return basic cinema information for all cinemas
|
|
50
|
+
# @return [Array<ScottUk::Cinema>]
|
|
51
|
+
# @example
|
|
52
|
+
# ScottUk::Cinema.all
|
|
53
|
+
# #=> [<ScottUk::Cinema>, <ScottUk::Cinema>, ...]
|
|
54
|
+
def self.all
|
|
55
|
+
VENUES.keys.map { |id| new(id) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @!method address
|
|
59
|
+
# Address of the cinema
|
|
60
|
+
# @return [Hash] of different address parts
|
|
61
|
+
# @see #adr
|
|
62
|
+
|
|
63
|
+
# Address of the cinema
|
|
64
|
+
# @return [Hash] contains :street_address, :extended_address,
|
|
65
|
+
# :locality, :region, :postal_code, :country_name
|
|
66
|
+
# @example
|
|
67
|
+
# cinema = ScottUk::Cinema.new('SCOBWR')
|
|
68
|
+
# cinema.adr
|
|
69
|
+
# #=> {
|
|
70
|
+
# street_address: 'Northgate Yard',
|
|
71
|
+
# extended_address: 'Mount Street',
|
|
72
|
+
# locality: 'Bridgwater',
|
|
73
|
+
# region: 'Somerset',
|
|
74
|
+
# postal_code: 'TA6 3FQ',
|
|
75
|
+
# country_name: 'United Kingdom'
|
|
76
|
+
# }
|
|
77
|
+
# @note Uses method naming as at http://microformats.org/wiki/adr
|
|
78
|
+
def adr
|
|
79
|
+
ADDRESS_PARTS.to_h { |part| [part, venue[part]] }.merge(country_name: 'United Kingdom')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Brand of the cinema
|
|
83
|
+
# @return [String] which will always be 'Scott Cinemas'
|
|
84
|
+
def brand
|
|
85
|
+
'Scott Cinemas'.freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Float, nil]
|
|
89
|
+
def latitude
|
|
90
|
+
venue[:latitude]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Float, nil]
|
|
94
|
+
def longitude
|
|
95
|
+
venue[:longitude]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The name of the cinema
|
|
99
|
+
# @return [String]
|
|
100
|
+
# @example
|
|
101
|
+
# ScottUk::Cinema.new('SCOORP').name
|
|
102
|
+
# #=> 'Bristol, Westbury Park'
|
|
103
|
+
def name
|
|
104
|
+
venue[:name].to_s
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The url of the cinema's pages on the Scott Cinemas website
|
|
108
|
+
# @return [String]
|
|
109
|
+
# @example
|
|
110
|
+
# ScottUk::Cinema.new('SCOBAR').url
|
|
111
|
+
# #=> 'https://barnstaple.scottcinemas.co.uk'
|
|
112
|
+
def url
|
|
113
|
+
subdomain = venue[:subdomain]
|
|
114
|
+
subdomain ? "https://#{subdomain}.scottcinemas.co.uk" : ''
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def venue
|
|
120
|
+
VENUES.fetch(id, {})
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScottUk
|
|
4
|
+
# Internal utility classes: Do not use
|
|
5
|
+
# @api private
|
|
6
|
+
module Internal
|
|
7
|
+
# Reads Scott Cinemas' public JSON listings feed, documented at
|
|
8
|
+
# https://www.scottcinemas.co.uk/api. Each venue's feed holds every
|
|
9
|
+
# advertised date.
|
|
10
|
+
class Feed
|
|
11
|
+
include Http
|
|
12
|
+
|
|
13
|
+
URL = 'https://www.scottcinemas.co.uk/json.php'
|
|
14
|
+
|
|
15
|
+
# @param [String] code a venue's feed code, e.g. 'SCOBAR'
|
|
16
|
+
# @return [Hash] the feed: 'cinema', 'films' by id, and 'schedule' of
|
|
17
|
+
# showtimes by local midnight, then screen, then start, each a Unix
|
|
18
|
+
# timestamp
|
|
19
|
+
def listings(code)
|
|
20
|
+
JSON.parse(get("#{URL}?cinema=#{CGI.escape(code)}"))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
|
|
5
|
+
module ScottUk
|
|
6
|
+
# Internal utility classes: Do not use
|
|
7
|
+
# @api private
|
|
8
|
+
module Internal
|
|
9
|
+
# Requests to scottcinemas.co.uk
|
|
10
|
+
module Http
|
|
11
|
+
RETRIES = 2
|
|
12
|
+
TRANSIENT_ERRORS = [
|
|
13
|
+
EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def get(url)
|
|
19
|
+
uri = URI(url)
|
|
20
|
+
attempts = 0
|
|
21
|
+
begin
|
|
22
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30) do |http|
|
|
23
|
+
http.request(Net::HTTP::Get.new(uri))
|
|
24
|
+
end
|
|
25
|
+
rescue *TRANSIENT_ERRORS
|
|
26
|
+
attempts += 1
|
|
27
|
+
raise if attempts > RETRIES
|
|
28
|
+
|
|
29
|
+
sleep(attempts)
|
|
30
|
+
retry
|
|
31
|
+
end
|
|
32
|
+
# Omit response bodies from errors, and never follow redirects.
|
|
33
|
+
raise "Scott Cinemas HTTP error #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
34
|
+
|
|
35
|
+
String.new(response.body, encoding: Encoding::UTF_8)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScottUk
|
|
4
|
+
# @api private
|
|
5
|
+
module Internal
|
|
6
|
+
module Parser
|
|
7
|
+
# Parses one showtime from the feed into performance attributes
|
|
8
|
+
class Showtime
|
|
9
|
+
# Showtime flags and the variants they mean. Others, such as 'STB'
|
|
10
|
+
# (strobe lighting), 'ATF' (all tickets £5), 'LUX' and 'DOL' (screen
|
|
11
|
+
# formats), are not variants.
|
|
12
|
+
FLAGS = {
|
|
13
|
+
'AFS' => 'relaxed',
|
|
14
|
+
'ART' => 'arts',
|
|
15
|
+
'BAL' => 'arts',
|
|
16
|
+
'ECA' => 'arts',
|
|
17
|
+
'ETV' => 'arts',
|
|
18
|
+
'MSC' => 'arts',
|
|
19
|
+
'MUS' => 'arts',
|
|
20
|
+
'OPE' => 'arts',
|
|
21
|
+
'PBB' => 'baby',
|
|
22
|
+
'SSC' => 'senior',
|
|
23
|
+
'THE' => 'arts'
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# Yes/no fields and the variants they mean. Most screenings offer
|
|
27
|
+
# audio description, so it does not mark one out.
|
|
28
|
+
FIELDS = {
|
|
29
|
+
'AFS' => 'relaxed',
|
|
30
|
+
'subtitles' => 'subtitled'
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
YES = 'Y'
|
|
34
|
+
FESTIVAL_FLAG = 'EVF'
|
|
35
|
+
FESTIVAL = 'Exmouth Film Festival'
|
|
36
|
+
THREE_D = /\A3D\z/i
|
|
37
|
+
LANGUAGE = /\((#{Cinebase::TitleSanitizer::LANGUAGES.join('|')})\)/i
|
|
38
|
+
|
|
39
|
+
# @param [Hash] showtime one showtime from the feed's schedule
|
|
40
|
+
# @param [Hash] film the feed's film for the showtime
|
|
41
|
+
# @param [Time] starting_at from the showtime's Unix timestamp
|
|
42
|
+
def initialize(showtime, film:, starting_at:)
|
|
43
|
+
@showtime = showtime
|
|
44
|
+
@film = film
|
|
45
|
+
@starting_at = starting_at
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Hash] options for ScottUk::Performance.new
|
|
49
|
+
def to_hash
|
|
50
|
+
{
|
|
51
|
+
booking_url: @showtime['booking_url'],
|
|
52
|
+
dimension: @showtime['performance_type'].to_s.match?(THREE_D) ? '3d' : '2d',
|
|
53
|
+
festival: festival,
|
|
54
|
+
film_name: TitleSanitizer.new(title).sanitized,
|
|
55
|
+
film_year: Cinebase::TitleSanitizer.film_year(title),
|
|
56
|
+
language: title[LANGUAGE, 1],
|
|
57
|
+
starting_at: @starting_at,
|
|
58
|
+
variant: variant
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def flags
|
|
65
|
+
Array(@showtime['flags'])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def festival
|
|
69
|
+
if flags.include?(FESTIVAL_FLAG)
|
|
70
|
+
Cinebase::Festival.new(name: FESTIVAL, year: @starting_at.year)
|
|
71
|
+
else
|
|
72
|
+
Cinebase::Festival.from_title(title, year: @starting_at.year)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def title
|
|
77
|
+
@title ||= CGI.unescapeHTML(@film.fetch('name')).strip
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def variant
|
|
81
|
+
from_fields = FIELDS.filter_map { |field, variant| variant if @showtime[field] == YES }
|
|
82
|
+
(flags.filter_map { |flag| FLAGS[flag] } + from_fields).uniq
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module ScottUk
|
|
2
|
+
# Internal utility classes: Do not use
|
|
3
|
+
# @api private
|
|
4
|
+
module Internal
|
|
5
|
+
# Sanitize and standardize film titles
|
|
6
|
+
class TitleSanitizer < Cinebase::TitleSanitizer
|
|
7
|
+
# @!method initialize(title)
|
|
8
|
+
# Constructor
|
|
9
|
+
# @param [String] title a film title
|
|
10
|
+
# @return [ScottUk::Internal::TitleSanitizer]
|
|
11
|
+
|
|
12
|
+
# @!method sanitized
|
|
13
|
+
# sanitized and standardized title
|
|
14
|
+
# @return [String] sanitised title
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# strings and regex to be removed
|
|
19
|
+
def remove
|
|
20
|
+
[
|
|
21
|
+
/\s*\(Parent and Baby Only Show\)/i, # parent and baby screening
|
|
22
|
+
/\s*(?:-\s+|\()Exmouth Film Festival\)?/i, # festival suffix
|
|
23
|
+
/\ASFS:\s*/ # film society prefix
|
|
24
|
+
] + COMMON_REMOVE
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# regexes and their replacements
|
|
28
|
+
def replace
|
|
29
|
+
{
|
|
30
|
+
/\ARBO \d{2}-\d{2}: (.*)/i => 'Royal Ballet and Opera: ',
|
|
31
|
+
/\ANATIONAL THEATRE LIVE: (.*)/i => 'National Theatre Live: '
|
|
32
|
+
}.merge(COMMON_REPLACE)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module ScottUk
|
|
2
|
+
# A single screening of a film on the Scott Cinemas website
|
|
3
|
+
class Performance < Cinebase::Performance
|
|
4
|
+
# @!attribute [r] booking_url
|
|
5
|
+
# @return [String] the booking URL on the cinema website
|
|
6
|
+
# @!attribute [r] cinema_name
|
|
7
|
+
# @return [String] the cinema name
|
|
8
|
+
# @!attribute [r] cinema_id
|
|
9
|
+
# @return [String] the cinema id
|
|
10
|
+
# @!attribute [r] dimension
|
|
11
|
+
# @return [String] 2d or 3d
|
|
12
|
+
# @!attribute [r] film_name
|
|
13
|
+
# @return [String] the film name
|
|
14
|
+
|
|
15
|
+
# @!method initialize(options)
|
|
16
|
+
# @param [Hash] options options hash
|
|
17
|
+
# @option options [String] :booking_url (nil) buying url for the screening
|
|
18
|
+
# @option options [String] :cinema_name name of the cinema
|
|
19
|
+
# @option options [String] :cinema_id website id of the cinema
|
|
20
|
+
# @option options [String] :dimension ('2d') dimension of the screening
|
|
21
|
+
# @option options [Cinebase::Festival] :festival (nil) festival edition
|
|
22
|
+
# @option options [String] :film_name name of the film
|
|
23
|
+
# @option options [Integer] :film_year (nil) year from the film's title
|
|
24
|
+
# @option options [String] :language (nil) audio language, e.g. 'Hindi'
|
|
25
|
+
# @option options [Time] :starting_at listed start time of the performance
|
|
26
|
+
|
|
27
|
+
TIME_ZONE = 'Europe/London'.freeze
|
|
28
|
+
|
|
29
|
+
# Performances within an inclusive range of showing dates, from one
|
|
30
|
+
# request: the venue's feed holds every advertised date.
|
|
31
|
+
# @param [String] cinema_id the cinema's feed code, e.g. 'SCOBAR'
|
|
32
|
+
# @param [Date] from first date (defaults to today in London)
|
|
33
|
+
# @param [Date] to last date (defaults to 13 days after from)
|
|
34
|
+
# @return [Array<ScottUk::Performance>]
|
|
35
|
+
def self.at(
|
|
36
|
+
cinema_id,
|
|
37
|
+
from: TZInfo::Timezone.get(TIME_ZONE).now.to_date,
|
|
38
|
+
to: from + 13
|
|
39
|
+
)
|
|
40
|
+
raise ArgumentError, 'to must not precede from' if to < from
|
|
41
|
+
|
|
42
|
+
cinema = Cinema.new(cinema_id)
|
|
43
|
+
if cinema.name.empty?
|
|
44
|
+
[]
|
|
45
|
+
else
|
|
46
|
+
feed = ScottUk::Internal::Feed.new.listings(cinema.id)
|
|
47
|
+
performances = showtimes_between(feed, from, to).map do |showtime, film, starting_at|
|
|
48
|
+
parsed = ScottUk::Internal::Parser::Showtime.new(showtime, film: film, starting_at: starting_at)
|
|
49
|
+
new(parsed.to_hash.merge(cinema_id: cinema.id, cinema_name: cinema.name))
|
|
50
|
+
end
|
|
51
|
+
performances.sort_by(&:starting_at)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @!method showing_on
|
|
56
|
+
# The date of the screening
|
|
57
|
+
# @return [Date]
|
|
58
|
+
|
|
59
|
+
# @!method starting_at
|
|
60
|
+
# UTC time of the screening
|
|
61
|
+
# @return [Time]
|
|
62
|
+
|
|
63
|
+
# @!method variant
|
|
64
|
+
# The kinds of screening (arts, audio_described, baby, relaxed ...)
|
|
65
|
+
# @return <Array[String]>
|
|
66
|
+
|
|
67
|
+
# PHP sends an empty schedule, day or screen as [] rather than {}
|
|
68
|
+
def self.showtimes_between(feed, from, to)
|
|
69
|
+
films = Hash(feed['films'])
|
|
70
|
+
Hash(feed['schedule']).flat_map do |midnight, screens|
|
|
71
|
+
if (from..to).cover?(london_date(midnight))
|
|
72
|
+
Hash(screens).values.flat_map do |showtimes|
|
|
73
|
+
Hash(showtimes).filter_map do |timestamp, showtime|
|
|
74
|
+
film = films[showtime['film_id']]
|
|
75
|
+
[showtime, film, Time.at(timestamp.to_i).utc] if film
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
[]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
private_class_method :showtimes_between
|
|
84
|
+
|
|
85
|
+
def self.london_date(timestamp)
|
|
86
|
+
TZInfo::Timezone.get(TIME_ZONE).to_local(Time.at(timestamp.to_i).utc).to_date
|
|
87
|
+
end
|
|
88
|
+
private_class_method :london_date
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/scott_uk.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'cgi/escape'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
require 'cinebase'
|
|
5
|
+
|
|
6
|
+
require_relative './scott_uk/version'
|
|
7
|
+
|
|
8
|
+
require_relative './scott_uk/internal/http'
|
|
9
|
+
require_relative './scott_uk/internal/feed'
|
|
10
|
+
require_relative './scott_uk/internal/title_sanitizer'
|
|
11
|
+
require_relative './scott_uk/internal/parser/showtime'
|
|
12
|
+
|
|
13
|
+
require_relative './scott_uk/cinema'
|
|
14
|
+
require_relative './scott_uk/performance'
|
|
15
|
+
|
|
16
|
+
# scott_uk gem
|
|
17
|
+
module ScottUk
|
|
18
|
+
end
|