omniplex_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.
data/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # OmniplexUk
2
+
3
+ A simple gem to pull cinemas and performances from the [Omniplex Cinemas website](https://www.omniplexcinemas.co.uk) and spit out useful formatted info.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'omniplex_uk'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install omniplex_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 classics,
24
+ encore and anniversary markers, language versions and years, and giving
25
+ Royal Ballet and Opera, Met Opera and National Theatre Live broadcasts the
26
+ standard prefixes.
27
+
28
+ No token or setup is needed. Omniplex's UK website runs on Admit One, but
29
+ with its own templates rather than the A1Web pages that cinebase's
30
+ `Cinebase::A1web` reads, so the gem has its own client. The gem covers the UK
31
+ venues only: Great Britain and Northern Ireland. The Republic of Ireland
32
+ venues are on omniplex.ie.
33
+
34
+ ### Cinema
35
+
36
+ The venue list is cached for an hour. Ids are the website's site codes, such
37
+ as `OMP_KENN`. The gem holds each venue's address and coordinates, from the
38
+ address block and directions link on its listings page. High Wycombe and
39
+ Sutton have neither, so theirs come from OpenStreetMap.
40
+
41
+ ``` ruby
42
+ OmniplexUk::Cinema.all
43
+ #=> [<OmniplexUk::Cinema ...>, <OmniplexUk::Cinema ...>, ...]
44
+
45
+ cinema = OmniplexUk::Cinema.new('OMP_KENN')
46
+ #=> <OmniplexUk::Cinema ...>
47
+
48
+ cinema.adr
49
+ #=> {
50
+ street_address: 'Kennedy Centre',
51
+ extended_address: '564-568 Falls Road',
52
+ locality: 'Belfast',
53
+ region: 'County Antrim',
54
+ postal_code: 'BT11 9AB',
55
+ country_name: 'United Kingdom'
56
+ }
57
+
58
+ cinema.brand
59
+ #=> 'Omniplex'
60
+
61
+ cinema.full_name
62
+ #=> 'Omniplex Belfast - Kennedy Centre'
63
+
64
+ cinema.id
65
+ #=> 'OMP_KENN'
66
+
67
+ cinema.latitude
68
+ #=> 54.58149
69
+
70
+ cinema.url
71
+ #=> 'https://www.omniplexcinemas.co.uk/cinema/showtimes?sitecode=OMP_KENN'
72
+ ```
73
+
74
+ ### Performances
75
+
76
+ `Performance.at` returns performances for an inclusive range of showing dates,
77
+ defaulting to today in London and the following 13 days. The website lists one
78
+ day per page. The first page also lists every date the venue advertises, so
79
+ the gem then reads only those dates in the range, one after another over one
80
+ connection. A year's range for one venue is about 45 to 60 requests.
81
+
82
+ ``` ruby
83
+ OmniplexUk::Performance.at('OMP_SUND', from: Date.today, to: Date.today + 2)
84
+ #=> [<OmniplexUk::Performance ...>, <OmniplexUk::Performance ...>, ...]
85
+
86
+ performance = OmniplexUk::Performance.at('OMP_SUND').first
87
+ #=> <OmniplexUk::Performance ...>
88
+
89
+ performance.film_name
90
+ #=> 'Clayface'
91
+
92
+ performance.language
93
+ #=> nil # e.g. 'Hindi' from '(Hindi)' in the title
94
+
95
+ performance.film_year
96
+ #=> nil # from the title, e.g. 1996 for 'Scream (1996) (Omniplex Classics)'
97
+
98
+ performance.dimension
99
+ #=> '2d'
100
+
101
+ performance.variant
102
+ #=> ['maxx']
103
+
104
+ performance.subtitled?
105
+ #=> false
106
+
107
+ performance.starting_at
108
+ #=> 2026-11-01 16:20:00 UTC
109
+
110
+ performance.booking_url
111
+ #=> 'https://www.omniplexcinemas.co.uk/booking/seats/sunderland/44797'
112
+
113
+ performance.cinema_name
114
+ #=> 'Sunderland'
115
+
116
+ performance.cinema_id
117
+ #=> 'OMP_SUND'
118
+ ```
119
+
120
+ Booking links go straight to seat selection for the performance.
121
+
122
+ Variants come from the hall icons and tags on each showtime, and the film's
123
+ Event genre:
124
+
125
+ | Website | Variant |
126
+ | --- | --- |
127
+ | Event genre, LIVE broadcasts | `arts` |
128
+ | Audio Described | `audio_described` |
129
+ | Avenue | `avenue` |
130
+ | D-BOX | `dbox` |
131
+ | 4D E-Motion | `4d` |
132
+ | Infinity Vision, Infinity Vision 3D | `infinity_vision` |
133
+ | MAXX | `maxx` |
134
+ | Seniors Club | `senior` |
135
+ | Sensory Friendly | `relaxed` |
136
+ | Sofa Bed | `sofabed` |
137
+ | Subtitled | `subtitled` |
138
+
139
+ Infinity Vision 3D showings are also `3d`.
140
+
141
+ ## Tests
142
+
143
+ ```sh
144
+ bundle exec rake # unit tests against fixtures
145
+ bundle exec rake live # live omniplexcinemas.co.uk
146
+ ```
147
+
148
+ Fixtures in `test/fixtures` are pages captured from omniplexcinemas.co.uk,
149
+ trimmed to a few films and showtimes, with the Sunderland calendar cut to the
150
+ dates captured. They keep the site's mix of UTF-8 and Windows-1252 bytes.
151
+ `test/support/api_shapes.rb` describes the parts of the pages the gem reads.
152
+ The unit suite checks the fixtures against it and the live suite checks real
153
+ responses, held only in memory. When the website changes, update the shape
154
+ first, then the fixtures.
155
+
156
+ ## Releasing
157
+
158
+ Bump the version, merge, then run the **Release Gem** workflow in GitHub
159
+ Actions. It runs the tests, tags the version and publishes the gem through
160
+ RubyGems trusted publishing.
161
+
162
+ ## Contributing
163
+
164
+ 1. Fork it
165
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
166
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
167
+ 4. Push to the branch (`git push origin my-new-feature`)
168
+ 5. Create new Pull Request
@@ -0,0 +1,172 @@
1
+ module OmniplexUk
2
+ # The object representing a cinema on the Omniplex Cinemas website
3
+ class Cinema < Cinebase::A1web::Cinema
4
+ # @!attribute [r] id
5
+ # @return [String] the Admit One site code of the cinema, e.g. 'OMP_KENN'
6
+
7
+ # @!method initialize(id)
8
+ # @param [String] id the cinema id, e.g. 'OMP_KENN' or 'omp_kenn'
9
+ # @return [OmniplexUk::Cinema]
10
+
11
+ # @!method self.all
12
+ # Return basic cinema information for the UK cinemas: Great Britain
13
+ # and Northern Ireland, not the Republic of Ireland
14
+ # @return [Array<OmniplexUk::Cinema>]
15
+ # @example
16
+ # OmniplexUk::Cinema.all
17
+ # #=> [<OmniplexUk::Cinema>, <OmniplexUk::Cinema>, ...]
18
+
19
+ # @!method adr
20
+ # Address of the cinema
21
+ # @return [Hash] contains :street_address, :extended_address,
22
+ # :locality, :region, :postal_code, :country_name
23
+ # @example
24
+ # cinema = OmniplexUk::Cinema.new('OMP_KENN')
25
+ # cinema.adr
26
+ # #=> {
27
+ # street_address: 'Kennedy Centre',
28
+ # extended_address: '564-568 Falls Road',
29
+ # locality: 'Belfast',
30
+ # region: 'County Antrim',
31
+ # postal_code: 'BT11 9AB',
32
+ # country_name: 'United Kingdom'
33
+ # }
34
+ # @note Uses method naming as at http://microformats.org/wiki/adr
35
+
36
+ # @!method latitude
37
+ # @return [Float, nil]
38
+
39
+ # @!method longitude
40
+ # @return [Float, nil]
41
+
42
+ # @!method name
43
+ # The name of the cinema
44
+ # @return [String]
45
+ # @example
46
+ # OmniplexUk::Cinema.new('OMP_KENN').name
47
+ # #=> 'Belfast - Kennedy Centre'
48
+
49
+ # @!method url
50
+ # The cinema's listings page, which also selects it on the website
51
+ # @return [String]
52
+ # @example
53
+ # OmniplexUk::Cinema.new('OMP_KENN').url
54
+ # #=> 'https://www.omniplexcinemas.co.uk/cinema/showtimes?sitecode=OMP_KENN'
55
+
56
+ # Addresses and coordinates from the address block and directions link
57
+ # on each venue's listings page, except High Wycombe and Sutton, which
58
+ # have neither and come from OpenStreetMap.
59
+ LOCATIONS = {
60
+ 'OMP_ANTR' => {
61
+ street_address: 'Junction One Retail Park', locality: 'Antrim', region: 'County Antrim',
62
+ postal_code: 'BT41 4LL', latitude: 54.7297, longitude: -6.2311
63
+ },
64
+ 'OMP_BANB' => {
65
+ street_address: 'Unit 4-12, The Outlet', locality: 'Banbridge', region: 'County Down',
66
+ postal_code: 'BT32 4GJ', latitude: 54.3326344, longitude: -6.2783001
67
+ },
68
+ 'OMP_BANG' => {
69
+ street_address: 'Valentines Road', locality: 'Bangor', region: 'County Down',
70
+ postal_code: 'BT20 4JH', latitude: 54.6548005, longitude: -5.6631795
71
+ },
72
+ 'OMP_BIRM' => {
73
+ street_address: 'Great Park', extended_address: 'Rubery', locality: 'Birmingham',
74
+ region: 'West Midlands', postal_code: 'B45 9JL', latitude: 52.39882, longitude: -2.00826
75
+ },
76
+ 'OMP_CARR' => {
77
+ street_address: 'Unit 3-4, Rodgers Quay', locality: 'Carrickfergus', region: 'County Antrim',
78
+ postal_code: 'BT38 8BE', latitude: 54.7123617, longitude: -5.8125379
79
+ },
80
+ 'OMP_CLYD' => {
81
+ street_address: '23 Britannia Way', locality: 'Clydebank', region: 'West Dunbartonshire',
82
+ postal_code: 'G81 2RZ', latitude: 55.9015, longitude: -4.4056
83
+ },
84
+ 'OMP_CRAI' => {
85
+ street_address: 'Highfield Heights', locality: 'Craigavon', region: 'County Armagh',
86
+ postal_code: 'BT64 3AE', latitude: 54.4492, longitude: -6.3985
87
+ },
88
+ 'OMP_DERR' => {
89
+ street_address: 'Quayside Centre', extended_address: 'Strand Road', locality: 'Derry',
90
+ region: 'County Londonderry', postal_code: 'BT48 7PX', latitude: 55.0002376, longitude: -7.3215562
91
+ },
92
+ 'OMP_DOWN' => {
93
+ street_address: '5 Owenbeg Avenue', locality: 'Downpatrick', region: 'County Down',
94
+ postal_code: 'BT30 6FJ', latitude: 54.3237579, longitude: -5.7139260
95
+ },
96
+ 'OMP_DUDO' => {
97
+ street_address: 'Entertainment Village', extended_address: 'Dundonald', locality: 'Belfast',
98
+ region: 'County Down', postal_code: 'BT16 1YG', latitude: 54.5874, longitude: -5.8189
99
+ },
100
+ 'OMP_DUGA' => {
101
+ street_address: 'Oaks Retail Park', extended_address: 'Oaks Road', locality: 'Dungannon',
102
+ region: 'County Tyrone', postal_code: 'BT71 4AR', latitude: 54.5125018, longitude: -6.7616527
103
+ },
104
+ 'OMP_GLAS' => {
105
+ street_address: '7 Renfrew Street', locality: 'Glasgow', region: 'Glasgow City',
106
+ postal_code: 'G2 3AB', latitude: 55.864976, longitude: -4.254477
107
+ },
108
+ 'OMP_HIWY' => {
109
+ street_address: 'Crest Road', locality: 'High Wycombe', region: 'Buckinghamshire',
110
+ postal_code: 'HP11 1UA', latitude: 51.6131257, longitude: -0.7719008
111
+ },
112
+ 'OMP_IPSW' => {
113
+ street_address: '1 Buttermarket Shopping Centre', locality: 'Ipswich', region: 'Suffolk',
114
+ postal_code: 'IP1 1DT', latitude: 52.0575, longitude: 1.1550
115
+ },
116
+ 'OMP_KENN' => {
117
+ street_address: 'Kennedy Centre', extended_address: '564-568 Falls Road', locality: 'Belfast',
118
+ region: 'County Antrim', postal_code: 'BT11 9AB', latitude: 54.58149, longitude: -5.97779
119
+ },
120
+ 'OMP_LARN' => {
121
+ street_address: 'Larne Leisure Park', extended_address: 'Redlands Road', locality: 'Larne',
122
+ region: 'County Antrim', postal_code: 'BT40 1FD', latitude: 54.8470005, longitude: -5.8102719
123
+ },
124
+ 'OMP_LISB' => {
125
+ street_address: 'Lisburn Leisure Park', extended_address: 'Governors Road', locality: 'Lisburn',
126
+ region: 'County Antrim', postal_code: 'BT28 1LP', latitude: 54.507481, longitude: -6.051616
127
+ },
128
+ 'OMP_NEWR' => {
129
+ street_address: 'The Quays Shopping Centre', locality: 'Newry', region: 'County Down',
130
+ postal_code: 'BT35 8QS', latitude: 54.1688, longitude: -6.3401
131
+ },
132
+ 'OMP_OMAG' => {
133
+ street_address: 'Kevlin Road', locality: 'Omagh', region: 'County Tyrone',
134
+ postal_code: 'BT78 1JL', latitude: 54.5965088, longitude: -7.3006357
135
+ },
136
+ 'OMP_SUND' => {
137
+ street_address: '4 Lambton Street', locality: 'Sunderland', region: 'Tyne and Wear',
138
+ postal_code: 'SR1 1TP', latitude: 54.9082446, longitude: -1.3800217
139
+ },
140
+ 'OMP_SUTT' => {
141
+ street_address: 'St Nicholas Centre', extended_address: 'St Nicholas Way', locality: 'Sutton',
142
+ region: 'Greater London', postal_code: 'SM1 1AY', latitude: 51.3647655, longitude: -0.1953102
143
+ },
144
+ 'OMP_WIGA' => {
145
+ street_address: 'Unit 2, Robin Leisure Park', locality: 'Wigan', region: 'Greater Manchester',
146
+ postal_code: 'WN5 0UJ', latitude: 53.54603, longitude: -2.65797
147
+ }
148
+ }.freeze
149
+
150
+ # @return [String]
151
+ def self.website
152
+ Internal::Client::WEBSITE
153
+ end
154
+
155
+ # @api private
156
+ # @return [OmniplexUk::Internal::Client]
157
+ def self.client
158
+ Internal::Client.new
159
+ end
160
+
161
+ # @api private
162
+ def self.locations
163
+ LOCATIONS
164
+ end
165
+
166
+ # Brand of the cinema
167
+ # @return [String] which will always be 'Omniplex'
168
+ def brand
169
+ 'Omniplex'.freeze
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,108 @@
1
+ require 'cgi/escape'
2
+ require 'net/http'
3
+
4
+ module OmniplexUk
5
+ # Internal utility classes: Do not use
6
+ # @api private
7
+ module Internal
8
+ # Reads the venue list and daily listings from omniplexcinemas.co.uk,
9
+ # Omniplex's Admit One site. A venue's listings take one request per
10
+ # day, so requests share one kept-alive connection until #close.
11
+ class Client
12
+ WEBSITE = 'https://www.omniplexcinemas.co.uk'.freeze
13
+ SHOWTIMES_PATH = '/cinema/showtimes'.freeze
14
+
15
+ # The home page's site picker groups venues by region. The Ireland
16
+ # region is the Republic, served by omniplex.ie.
17
+ REGION_OR_VENUE = /data-region="(\w+)"|data-name="([^"]*)" data-sitecode="(\w+)"/
18
+ UK_REGIONS = %w(England NorthernIreland Scotland Wales).freeze
19
+
20
+ # Pages claim ISO-8859-1 but mix UTF-8 with Windows-1252 apostrophes
21
+ LEGACY_ENCODING = Encoding::Windows_1252
22
+ RETRIES = 2
23
+ TRANSIENT_ERRORS = [
24
+ EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
25
+ ].freeze
26
+
27
+ # UK venues in the home page's site picker
28
+ # @return [Array<Hash>] with :url, :name and :code, e.g.
29
+ # { url: 'https://www.omniplexcinemas.co.uk/cinema/showtimes?sitecode=OMP_KENN',
30
+ # name: 'Belfast - Kennedy Centre', code: 'OMP_KENN' }
31
+ def venues
32
+ region = nil
33
+ venues = get('/').scan(REGION_OR_VENUE).filter_map do |group, name, code|
34
+ if group
35
+ region = group
36
+ nil
37
+ elsif UK_REGIONS.include?(region)
38
+ { url: showtimes_url(code), name: CGI.unescapeHTML(name).strip, code: code }
39
+ end
40
+ end
41
+ venues.uniq { |venue| venue[:code] }
42
+ end
43
+
44
+ # One day's listings for a venue, or its default day's without a date.
45
+ # Each page also lists every advertised date.
46
+ # @param [String] site_code e.g. 'OMP_KENN'
47
+ # @param [Date, nil] date
48
+ # @return [String]
49
+ def showtimes(site_code, date = nil)
50
+ get(showtimes_path(site_code, date))
51
+ end
52
+
53
+ # @param [String] site_code e.g. 'OMP_KENN'
54
+ # @return [String] the venue's listings page; it also selects the venue
55
+ # on the website for later visits
56
+ def showtimes_url(site_code)
57
+ WEBSITE + showtimes_path(site_code)
58
+ end
59
+
60
+ def close
61
+ @http.finish if @http&.started?
62
+ @http = nil
63
+ end
64
+
65
+ private
66
+
67
+ def showtimes_path(site_code, date = nil)
68
+ query = { sitecode: site_code }
69
+ query.merge!(action: 'processFilters', filterDate: date.iso8601) if date
70
+ "#{SHOWTIMES_PATH}?#{URI.encode_www_form(query)}"
71
+ end
72
+
73
+ def get(path)
74
+ attempts = 0
75
+ begin
76
+ response = http.request(Net::HTTP::Get.new(path))
77
+ rescue *TRANSIENT_ERRORS
78
+ close
79
+ attempts += 1
80
+ raise if attempts > RETRIES
81
+
82
+ sleep(attempts)
83
+ retry
84
+ end
85
+
86
+ if response.is_a?(Net::HTTPSuccess)
87
+ decode(response.body)
88
+ else
89
+ # Omit response bodies from errors, and never follow redirects.
90
+ raise "HTTP error #{response.code} from #{URI(WEBSITE).host}"
91
+ end
92
+ end
93
+
94
+ def http
95
+ @http ||= begin
96
+ uri = URI(WEBSITE)
97
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 30, max_retries: 0)
98
+ end
99
+ end
100
+
101
+ def decode(body)
102
+ body.dup.force_encoding(Encoding::UTF_8).scrub do |bytes|
103
+ bytes.force_encoding(LEGACY_ENCODING).encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,100 @@
1
+ require 'cgi/escape'
2
+ require 'date'
3
+
4
+ module OmniplexUk
5
+ # Internal utility classes: Do not use
6
+ # @api private
7
+ module Internal
8
+ # Reads one day's showtimes, and the venue's advertised dates, from a
9
+ # listings page.
10
+ class Listings
11
+ # The calendar's dates are local midnights in milliseconds, so dates
12
+ # in British Summer Time fall at 23:00 UTC the day before
13
+ ADVERTISED_DATES = /allowedDatesTimestamps = \[([\d,\s]*)\]/
14
+ PAGE_DATE = /var defaultDate = "(\d{4}-\d{2}-\d{2})"/
15
+ FILM = %r{
16
+ <p\sclass="font-medium\stext-lg[^"]*">\s*<a\s[^>]*>(.*?)</a>\s*</p>
17
+ \s*<img\s[^>]*id="favourite([^"]+)"
18
+ }mx
19
+ GENRES = %r{/images/ratings/[^"]*"[^>]*>\s*<p[^>]*>([^<]*)</p>}
20
+ SHOWTIME = %r{<a aria-label="[^"]*? at (\d{1,2}):(\d{2}) in [^"]*" href="([^"]*)"[^>]*>(.*?)</a>}m
21
+ ICON = %r{<img src="[^"]*/([\w-]+)\.(?:png|svg)"}
22
+ TAG = %r{<span class="text-xs[^"]*">([^<]*)</span>}
23
+
24
+ # Hall icons on a showtime, named as the website names them
25
+ ICONS = {
26
+ '4d_emotion' => '4D E-Motion',
27
+ 'avenue' => 'Avenue',
28
+ 'infinity3d_icon' => 'Infinity Vision 3D',
29
+ 'infinity_icon' => 'Infinity Vision',
30
+ 'omniplex_mobile-app-2025_svgs_audio-described-alt' => 'Audio Described',
31
+ 'omniplex_mobile-app-2025_svgs_dbox-orange' => 'D-BOX',
32
+ 'omniplex_mobile-app-2025_svgs_maxx' => 'MAXX',
33
+ 'sofabed' => 'Sofa Bed'
34
+ }.freeze
35
+
36
+ # Films whose genres include it are live and recorded events
37
+ EVENT_GENRE = 'Event'.freeze
38
+ MILLISECONDS = 1000
39
+ TIME_ZONE = 'Europe/London'.freeze
40
+
41
+ # @return [Date, nil] the day this page lists
42
+ attr_reader :date
43
+
44
+ # @param [String] html a listings page
45
+ def initialize(html)
46
+ @html = html
47
+ page_date = html[PAGE_DATE, 1]
48
+ @date = Date.iso8601(page_date) if page_date
49
+ end
50
+
51
+ # @return [Array<Date>] every date the venue advertises, in order
52
+ def dates
53
+ timestamps = @html[ADVERTISED_DATES, 1].to_s.split(',')
54
+ timestamps.map { |milliseconds| local_date(milliseconds.to_i / MILLISECONDS) }.uniq.sort
55
+ end
56
+
57
+ # @return [Array<Cinebase::A1web::Showing>]
58
+ def showings
59
+ if date
60
+ @html.split(FILM).drop(1).each_slice(3).flat_map do |title, film_id, markup|
61
+ film_showings(text(title), film_id, markup.to_s)
62
+ end
63
+ else
64
+ []
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def film_showings(title, film_id, markup)
71
+ genres = markup[GENRES, 1].to_s.split(',').map(&:strip)
72
+ film_tags = genres.include?(EVENT_GENRE) ? [EVENT_GENRE] : []
73
+ markup.scan(SHOWTIME).map do |hour, minute, href, box|
74
+ Cinebase::A1web::Showing.new(
75
+ film_id: film_id, title: title, date: date, starting_at: starting_at(hour.to_i, minute.to_i),
76
+ booking_url: CGI.unescapeHTML(href), tags: film_tags + tags(box)
77
+ )
78
+ end
79
+ end
80
+
81
+ def tags(box)
82
+ icons = box.scan(ICON).flatten.filter_map { |icon| ICONS[icon] }
83
+ icons + box.scan(TAG).flatten.map { |tag| text(tag) }
84
+ end
85
+
86
+ def local_date(seconds)
87
+ TZInfo::Timezone.get(TIME_ZONE).to_local(Time.at(seconds).utc).to_date
88
+ end
89
+
90
+ def starting_at(hour, minute)
91
+ local = Time.utc(date.year, date.month, date.day, hour, minute)
92
+ TZInfo::Timezone.get(TIME_ZONE).local_to_utc(local) { |periods| periods.first }.utc
93
+ end
94
+
95
+ def text(html)
96
+ CGI.unescapeHTML(html.gsub(/<[^>]*>/, ' ')).gsub(/\s+/, ' ').strip
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,43 @@
1
+ module OmniplexUk
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
+ # The website loses some apostrophes, e.g. 'Andre Rieu?s'
8
+ LOST_APOSTROPHE = /(\w)\?(s)\b/
9
+
10
+ # @!method initialize(title)
11
+ # Constructor
12
+ # @param [String] title a film title
13
+ # @return [OmniplexUk::Internal::TitleSanitizer]
14
+
15
+ # sanitized and standardized title
16
+ # @return [String] sanitised title
17
+ def sanitized
18
+ @restored ||= super.gsub(LOST_APOSTROPHE, "\\1'\\2")
19
+ end
20
+
21
+ private
22
+
23
+ # strings and regex to be removed
24
+ def remove
25
+ [
26
+ /\s*\(Omniplex Classics\)/i, # classics season
27
+ /\s*\(Sunrise Screening\)/i, # early screening
28
+ /\s*\(Poland\)/, # Polish film
29
+ /\s+Encore\z/i # encore screening
30
+ ] + COMMON_REMOVE
31
+ end
32
+
33
+ # regexes and their replacements
34
+ def replace
35
+ {
36
+ /\A(.*?)\s+-\s*LIVE\s*-\s*The Royal (?:Ballet|Opera) \d{2}-\d{2}\z/ => 'Royal Ballet and Opera: ',
37
+ /\A(.*?)\s+-\s+The Met Opera \d{2}-\d{2}\z/ => 'Met Opera: ',
38
+ /\ANT Live:\s*(.*)/ => 'National Theatre Live: '
39
+ }.merge(COMMON_REPLACE)
40
+ end
41
+ end
42
+ end
43
+ end