light_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 +150 -0
- data/lib/light_uk/cinema.rb +112 -0
- data/lib/light_uk/internal/http.rb +27 -0
- data/lib/light_uk/internal/parser/coming_soon.rb +33 -0
- data/lib/light_uk/internal/parser/session.rb +97 -0
- data/lib/light_uk/internal/title_sanitizer.rb +38 -0
- data/lib/light_uk/internal/website.rb +48 -0
- data/lib/light_uk/performance.rb +77 -0
- data/lib/light_uk/version.rb +6 -0
- data/lib/light_uk.rb +20 -0
- metadata +152 -0
data/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# LightUk
|
|
2
|
+
|
|
3
|
+
A simple gem to pull cinemas and performances from the [Light cinemas website](https://thelight.co.uk) and spit out useful formatted info.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
gem 'light_uk'
|
|
10
|
+
|
|
11
|
+
And then execute:
|
|
12
|
+
|
|
13
|
+
$ bundle
|
|
14
|
+
|
|
15
|
+
Or install it yourself as:
|
|
16
|
+
|
|
17
|
+
$ gem install light_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 language
|
|
24
|
+
versions, years, anniversary restorations, Q&A add-ons and festival prefixes.
|
|
25
|
+
|
|
26
|
+
No token or setup is needed. Each venue has its own site, such as
|
|
27
|
+
`https://sheffield.thelight.co.uk`, built by Glitterfish. Every venue's home
|
|
28
|
+
page lists all the venues and shows its own address.
|
|
29
|
+
|
|
30
|
+
### Cinema
|
|
31
|
+
|
|
32
|
+
The venue list is cached for an hour. The first call to an address method reads
|
|
33
|
+
the cinema's home page. The website gives no coordinates, so `latitude` and
|
|
34
|
+
`longitude` are `nil`.
|
|
35
|
+
|
|
36
|
+
``` ruby
|
|
37
|
+
LightUk::Cinema.all
|
|
38
|
+
#=> [<LightUk::Cinema ...>, <LightUk::Cinema ...>, ...]
|
|
39
|
+
|
|
40
|
+
cinema = LightUk::Cinema.new(3)
|
|
41
|
+
#=> <LightUk::Cinema ...>
|
|
42
|
+
|
|
43
|
+
cinema.adr
|
|
44
|
+
#=> {
|
|
45
|
+
street_address: 'Cambridge Leisure Park',
|
|
46
|
+
extended_address: 'Clifton Way',
|
|
47
|
+
locality: 'Cambridge',
|
|
48
|
+
region: nil,
|
|
49
|
+
postal_code: 'CB1 7DY',
|
|
50
|
+
country_name: 'United Kingdom'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cinema.brand
|
|
54
|
+
#=> 'Light'
|
|
55
|
+
|
|
56
|
+
cinema.full_name
|
|
57
|
+
#=> 'Light Cambridge'
|
|
58
|
+
|
|
59
|
+
cinema.id
|
|
60
|
+
#=> '3'
|
|
61
|
+
|
|
62
|
+
cinema.url
|
|
63
|
+
#=> 'https://cambridge.thelight.co.uk'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Performances
|
|
67
|
+
|
|
68
|
+
`Performance.at` returns performances for an inclusive range of showing dates,
|
|
69
|
+
defaulting to today in London and the following 13 days. The venue's guide only
|
|
70
|
+
covers a fortnight, so it also reads the coming soon page, which holds every
|
|
71
|
+
session of films that have not opened yet, however far ahead. That is two
|
|
72
|
+
requests for all of the cinema's advertised dates.
|
|
73
|
+
|
|
74
|
+
``` ruby
|
|
75
|
+
LightUk::Performance.at(3, from: Date.today, to: Date.today + 2)
|
|
76
|
+
#=> [<LightUk::Performance ...>, <LightUk::Performance ...>, ...]
|
|
77
|
+
|
|
78
|
+
performance = LightUk::Performance.at(3).first
|
|
79
|
+
#=> <LightUk::Performance ...>
|
|
80
|
+
|
|
81
|
+
performance.film_name
|
|
82
|
+
#=> 'Ramayana: Rise of a Legend'
|
|
83
|
+
|
|
84
|
+
performance.language
|
|
85
|
+
#=> 'Hindi' # from '(Hindi)' in the title
|
|
86
|
+
|
|
87
|
+
performance.film_year
|
|
88
|
+
#=> nil # from the title, e.g. 1996 for 'From Dusk Till Dawn (1996)'
|
|
89
|
+
|
|
90
|
+
performance.dimension
|
|
91
|
+
#=> '3d'
|
|
92
|
+
|
|
93
|
+
performance.variant
|
|
94
|
+
#=> ['imax']
|
|
95
|
+
|
|
96
|
+
performance.subtitled?
|
|
97
|
+
#=> false
|
|
98
|
+
|
|
99
|
+
performance.festival
|
|
100
|
+
#=> nil # e.g. #<data Cinebase::Festival name="CFF", year=2026>
|
|
101
|
+
|
|
102
|
+
performance.starting_at
|
|
103
|
+
#=> 2026-11-08 19:50:00 UTC
|
|
104
|
+
|
|
105
|
+
performance.booking_url
|
|
106
|
+
#=> 'https://cambridge.thelight.co.uk/#!book/cinema/83478'
|
|
107
|
+
|
|
108
|
+
performance.cinema_name
|
|
109
|
+
#=> 'Cambridge'
|
|
110
|
+
|
|
111
|
+
performance.cinema_id
|
|
112
|
+
#=> '3'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Booking links open the booking panel on the venue's home page. Sessions that
|
|
116
|
+
have started have no booking link.
|
|
117
|
+
|
|
118
|
+
Variants are `arts`, `audio_described`, `baby`, `imax`, `kids`, `q&a`,
|
|
119
|
+
`relaxed`, `senior` and `subtitled`.
|
|
120
|
+
|
|
121
|
+
## Tests
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
bundle exec rake # unit tests against fixtures
|
|
125
|
+
bundle exec rake live # live thelight.co.uk
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Fixtures in `test/fixtures` are pages captured from the Cambridge venue's
|
|
129
|
+
website and trimmed to a few films. `test/support/api_shapes.rb` describes the
|
|
130
|
+
fields the gem reads. The unit suite checks the fixtures against it and the
|
|
131
|
+
live suite checks real responses, held only in memory. When the website
|
|
132
|
+
changes, update the shape first, then the fixtures.
|
|
133
|
+
|
|
134
|
+
The website's certificate chains to Sectigo's Public Server Authentication Root
|
|
135
|
+
R46. Trust stores without it, such as macOS's `/etc/ssl/cert.pem`, fail to
|
|
136
|
+
verify it; point `SSL_CERT_FILE` at a current Mozilla bundle instead.
|
|
137
|
+
|
|
138
|
+
## Releasing
|
|
139
|
+
|
|
140
|
+
Bump the version, merge, then run the **Release Gem** workflow in GitHub
|
|
141
|
+
Actions. It runs the tests, tags the version and publishes the gem through
|
|
142
|
+
RubyGems trusted publishing.
|
|
143
|
+
|
|
144
|
+
## Contributing
|
|
145
|
+
|
|
146
|
+
1. Fork it
|
|
147
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
148
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
149
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
150
|
+
5. Create new Pull Request
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module LightUk
|
|
2
|
+
# The object representing a cinema on the Light website
|
|
3
|
+
class Cinema < Cinebase::Cinema
|
|
4
|
+
# @!attribute [r] id
|
|
5
|
+
# @return [String] the Light id of the cinema, e.g. '7'
|
|
6
|
+
|
|
7
|
+
# Worker processes outlive a day's listings, so refetch the venue list
|
|
8
|
+
CACHE_SECONDS = 3600
|
|
9
|
+
|
|
10
|
+
# UK postcode, the last part of an address
|
|
11
|
+
POSTCODE = /\A[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}\z/i.freeze
|
|
12
|
+
|
|
13
|
+
# Constructor
|
|
14
|
+
# @param [Integer, String] id the cinema id of the format 7/'7'
|
|
15
|
+
# @return [LightUk::Cinema]
|
|
16
|
+
def initialize(id)
|
|
17
|
+
@id = id.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Return basic cinema information for all cinemas
|
|
21
|
+
# @return [Array<LightUk::Cinema>]
|
|
22
|
+
# @example
|
|
23
|
+
# LightUk::Cinema.all
|
|
24
|
+
# #=> [<LightUk::Cinema>, <LightUk::Cinema>, ...]
|
|
25
|
+
def self.all
|
|
26
|
+
sites_hash.keys.map { |id| new(id) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @!method address
|
|
30
|
+
# Address of the cinema
|
|
31
|
+
# @return [Hash] of different address parts
|
|
32
|
+
# @see #adr
|
|
33
|
+
|
|
34
|
+
# Address of the cinema, read from the footer of its home page
|
|
35
|
+
# @return [Hash] contains :street_address, :extended_address,
|
|
36
|
+
# :locality, :region, :postal_code, :country_name
|
|
37
|
+
# @example
|
|
38
|
+
# cinema = LightUk::Cinema.new(3)
|
|
39
|
+
# cinema.adr
|
|
40
|
+
# #=> {
|
|
41
|
+
# street_address: 'Cambridge Leisure Park',
|
|
42
|
+
# extended_address: 'Clifton Way',
|
|
43
|
+
# locality: 'Cambridge',
|
|
44
|
+
# region: nil,
|
|
45
|
+
# postal_code: 'CB1 7DY',
|
|
46
|
+
# country_name: 'United Kingdom'
|
|
47
|
+
# }
|
|
48
|
+
# @note Uses method naming as at http://microformats.org/wiki/adr
|
|
49
|
+
def adr
|
|
50
|
+
parts = address_parts
|
|
51
|
+
postal_code = parts.pop if parts.last&.match?(POSTCODE)
|
|
52
|
+
# Some addresses leave out the town, which every venue is named after
|
|
53
|
+
town_at = parts.index(name) || parts.size
|
|
54
|
+
lines = parts.first(town_at)
|
|
55
|
+
region = parts.drop(town_at + 1)
|
|
56
|
+
{
|
|
57
|
+
street_address: lines.shift,
|
|
58
|
+
extended_address: lines.empty? ? nil : lines.join(', '),
|
|
59
|
+
locality: name,
|
|
60
|
+
region: region.empty? ? nil : region.join(', '),
|
|
61
|
+
postal_code: postal_code,
|
|
62
|
+
country_name: 'United Kingdom'.freeze
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Brand of the cinema
|
|
67
|
+
# @return [String] which will always be 'Light'
|
|
68
|
+
def brand
|
|
69
|
+
'Light'.freeze
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The name of the cinema
|
|
73
|
+
# @return [String]
|
|
74
|
+
# @example
|
|
75
|
+
# LightUk::Cinema.new(7).name
|
|
76
|
+
# #=> 'Sheffield'
|
|
77
|
+
def name
|
|
78
|
+
site['title'].to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The url of the cinema's home page on the Light website
|
|
82
|
+
# @return [String]
|
|
83
|
+
# @example
|
|
84
|
+
# LightUk::Cinema.new(7).url
|
|
85
|
+
# #=> 'https://sheffield.thelight.co.uk'
|
|
86
|
+
def url
|
|
87
|
+
site['url'].to_s
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @api private
|
|
91
|
+
def self.sites_hash
|
|
92
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
93
|
+
if @sites_hash.nil? || now - @fetched_at > CACHE_SECONDS
|
|
94
|
+
@fetched_at = now
|
|
95
|
+
@sites_hash = LightUk::Internal::Website.new.sites.to_h { |site| [site.fetch('id').to_s, site] }
|
|
96
|
+
end
|
|
97
|
+
@sites_hash
|
|
98
|
+
end
|
|
99
|
+
private_class_method :sites_hash
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def address_parts
|
|
104
|
+
@address ||= LightUk::Internal::Website.new.address(url).to_s
|
|
105
|
+
@address.split(',').map(&:strip).reject(&:empty?)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def site
|
|
109
|
+
self.class.send(:sites_hash).fetch(id, {})
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
|
|
5
|
+
module LightUk
|
|
6
|
+
# Internal utility classes: Do not use
|
|
7
|
+
# @api private
|
|
8
|
+
module Internal
|
|
9
|
+
# Requests to thelight.co.uk
|
|
10
|
+
module Http
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def get(url)
|
|
14
|
+
uri = URI(url)
|
|
15
|
+
request = Net::HTTP::Get.new(uri)
|
|
16
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
|
|
17
|
+
open_timeout: 10, read_timeout: 30) do |http|
|
|
18
|
+
http.request(request)
|
|
19
|
+
end
|
|
20
|
+
# Omit response bodies from errors, and never follow redirects.
|
|
21
|
+
raise "Light HTTP error #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
22
|
+
|
|
23
|
+
String.new(response.body, encoding: Encoding::UTF_8)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module LightUk
|
|
6
|
+
# @api private
|
|
7
|
+
module Internal
|
|
8
|
+
module Parser
|
|
9
|
+
# Reads the films and sessions embedded in a venue's coming soon page
|
|
10
|
+
class ComingSoon
|
|
11
|
+
# Each film's markup starts with its programme id
|
|
12
|
+
FILM = /<div data-prog="(\d+)" class="prog">/
|
|
13
|
+
TITLE = %r{<h2><a href="[^"]*">(.*?)</a></h2>}m
|
|
14
|
+
|
|
15
|
+
# @param [String] html the coming soon page
|
|
16
|
+
def initialize(html)
|
|
17
|
+
@html = html
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Array<Hash>] films in the shape of the miniguide's Schedule
|
|
21
|
+
def films
|
|
22
|
+
@html.split(FILM).drop(1).each_slice(2).filter_map do |id, markup|
|
|
23
|
+
title = markup[TITLE, 1]
|
|
24
|
+
dates = markup[/ScheduleBrowser\(\{selector: '#prog-#{id}'.*?, data:(\[.*?\])\}\)\}\);<\/script>/m, 1]
|
|
25
|
+
if title && dates
|
|
26
|
+
{ "ID" => id.to_i, "Title" => title, "Dates" => JSON.parse(dates) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LightUk
|
|
4
|
+
# @api private
|
|
5
|
+
module Internal
|
|
6
|
+
module Parser
|
|
7
|
+
# Parses a single Light session into performance attributes
|
|
8
|
+
class Session
|
|
9
|
+
# Booking opens in a panel on the venue's home page
|
|
10
|
+
BOOKING_PATH = "/#!book/cinema/"
|
|
11
|
+
|
|
12
|
+
# Light collection titles
|
|
13
|
+
VARIANTS = {
|
|
14
|
+
"Audio Description" => "audio_described",
|
|
15
|
+
"Baby-friendly screenings" => "baby",
|
|
16
|
+
"Captioned for hard of hearing" => "subtitled",
|
|
17
|
+
"Dementia-friendly screenings" => "relaxed",
|
|
18
|
+
"Family Specials" => "kids",
|
|
19
|
+
"IMAX" => "imax",
|
|
20
|
+
"Museums & Galleries on Screen" => "arts",
|
|
21
|
+
"Music" => "arts",
|
|
22
|
+
"Musicals" => "arts",
|
|
23
|
+
"Opera & Dance" => "arts",
|
|
24
|
+
"Q&A" => "q&a",
|
|
25
|
+
"Relaxed Screenings" => "relaxed",
|
|
26
|
+
"Silver Screen" => "senior",
|
|
27
|
+
"Theatre" => "arts",
|
|
28
|
+
"Toddler Time" => "kids"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# FormatDisplay, e.g. '2D', '3D', 'IMAX' or 'IMAX 3D'
|
|
32
|
+
THREE_D = /\b3D\b/i
|
|
33
|
+
IMAX = /\bIMAX\b/i
|
|
34
|
+
|
|
35
|
+
LANGUAGE = /\((#{Cinebase::TitleSanitizer::LANGUAGES.join('|')})\)/i
|
|
36
|
+
TIME_ZONE = "Europe/London"
|
|
37
|
+
|
|
38
|
+
# @param [Hash] session one session
|
|
39
|
+
# @param [Hash] film the film the session shows
|
|
40
|
+
# @param [Hash] date the listings date the session is on
|
|
41
|
+
# @param [String] venue_url the venue's home page
|
|
42
|
+
def initialize(session, film:, date:, venue_url:)
|
|
43
|
+
@session = session
|
|
44
|
+
@film = film
|
|
45
|
+
@date = date
|
|
46
|
+
@venue_url = venue_url
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Hash] options for LightUk::Performance.new
|
|
50
|
+
def to_hash
|
|
51
|
+
{
|
|
52
|
+
booking_url: booking_url,
|
|
53
|
+
dimension: format.match?(THREE_D) ? "3d" : "2d",
|
|
54
|
+
festival: Cinebase::Festival.from_title(title, year: starting_at.year),
|
|
55
|
+
film_name: TitleSanitizer.new(title).sanitized,
|
|
56
|
+
film_year: Cinebase::TitleSanitizer.film_year(title),
|
|
57
|
+
language: title[LANGUAGE, 1],
|
|
58
|
+
starting_at: starting_at,
|
|
59
|
+
variant: variant
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Sessions that have started have no booking id
|
|
66
|
+
def booking_url
|
|
67
|
+
id = @session["BOID"]
|
|
68
|
+
"#{@venue_url.chomp('/')}#{BOOKING_PATH}#{id}" if id
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def format
|
|
72
|
+
@session["FormatDisplay"].to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Dates are keys such as '20261031' and times are London time such as '18.15'
|
|
76
|
+
def starting_at
|
|
77
|
+
@starting_at ||= begin
|
|
78
|
+
year, month, day = @date.fetch("Key").unpack("a4a2a2").map(&:to_i)
|
|
79
|
+
hour, minute = @session.fetch("Display").scan(/\d+/).map(&:to_i)
|
|
80
|
+
local = Time.utc(year, month, day, hour, minute)
|
|
81
|
+
TZInfo::Timezone.get(TIME_ZONE).local_to_utc(local) { |periods| periods.first }.utc
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def title
|
|
86
|
+
@title ||= CGI.unescapeHTML(@film.fetch("Title"))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def variant
|
|
90
|
+
variants = @session.fetch("Collections", []).filter_map { |collection| VARIANTS[collection["Title"]] }
|
|
91
|
+
variants << "imax" if format.match?(IMAX)
|
|
92
|
+
variants.uniq
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module LightUk
|
|
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 [LightUk::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
|
+
/\ACrafty Cinema:\s*/i, # craft activity before the film
|
|
22
|
+
/\s*\(\d+(st|nd|rd|th) Anniversary [^)]*\)/i, # anniversary restoration
|
|
23
|
+
/\s*\(4K Restoration\)/i, # restoration
|
|
24
|
+
/\s*\(Encore\)/i, # encore broadcast
|
|
25
|
+
/\s*\+\s*Mulled Wine\b.*\z/i # seasonal extras
|
|
26
|
+
] + COMMON_REMOVE
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# regexes and their replacements
|
|
30
|
+
def replace
|
|
31
|
+
{
|
|
32
|
+
/EXHIBITION ON SCREEN:\s*(.*)/i => 'Exhibition on Screen: ',
|
|
33
|
+
/(?:NT|National Theatre) Live:\s*(.*)/ => 'National Theatre Live: '
|
|
34
|
+
}.merge(COMMON_REPLACE)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module LightUk
|
|
6
|
+
# Internal utility classes: Do not use
|
|
7
|
+
# @api private
|
|
8
|
+
module Internal
|
|
9
|
+
# Reads venues and listings from the Light websites, one per venue
|
|
10
|
+
class Website
|
|
11
|
+
include Http
|
|
12
|
+
|
|
13
|
+
# Every venue's home page lists all the venues
|
|
14
|
+
SITES_URL = "https://sheffield.thelight.co.uk/"
|
|
15
|
+
MINIGUIDE_PATH = "/resource/services/miniguide/data.ashx"
|
|
16
|
+
COMING_SOON_PATH = "/cinema/comingsoon"
|
|
17
|
+
|
|
18
|
+
# @return [Array<Hash>] venues, e.g. { 'id' => 7, 'title' => 'Sheffield',
|
|
19
|
+
# 'url' => 'https://sheffield.thelight.co.uk' }
|
|
20
|
+
def sites
|
|
21
|
+
JSON.parse(get(SITES_URL)[/var __sites = (\[.*?\]);/, 1] || "[]")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param [String] url a venue's home page
|
|
25
|
+
# @return [String, nil] the address in the page footer, e.g.
|
|
26
|
+
# 'The Moor, Sheffield, S1 4PF'
|
|
27
|
+
def address(url)
|
|
28
|
+
address = get(url)[%r{<p class="address">(.*?)</p>}m, 1]
|
|
29
|
+
CGI.unescapeHTML(address).strip if address
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The guide behind the venue's listings, which covers a fortnight
|
|
33
|
+
# @param [String] url a venue's home page
|
|
34
|
+
# @return [Array<Hash>] films with their sessions on each date
|
|
35
|
+
def miniguide(url)
|
|
36
|
+
script = get(URI.join(url, MINIGUIDE_PATH).to_s)
|
|
37
|
+
JSON.parse(script[/var __gfminiguidedata = (\{.*\});/m, 1] || "{}").fetch("Schedule", [])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Every session of films not yet showing, however far ahead
|
|
41
|
+
# @param [String] url a venue's home page
|
|
42
|
+
# @return [Array<Hash>] films in the shape of #miniguide
|
|
43
|
+
def coming_soon(url)
|
|
44
|
+
Parser::ComingSoon.new(get(URI.join(url, COMING_SOON_PATH).to_s)).films
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module LightUk
|
|
2
|
+
# A single screening of a film on the Light 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
|
+
# Performances within an inclusive range of showing dates, from two
|
|
28
|
+
# requests for all of the cinema's advertised dates: the next fortnight's
|
|
29
|
+
# guide and the coming soon page.
|
|
30
|
+
# @param [Integer, String] cinema_id id of the cinema
|
|
31
|
+
# @param [Date] from first date (defaults to today in London)
|
|
32
|
+
# @param [Date] to last date (defaults to 13 days after from)
|
|
33
|
+
# @return [Array<LightUk::Performance>]
|
|
34
|
+
def self.at(
|
|
35
|
+
cinema_id,
|
|
36
|
+
from: TZInfo::Timezone.get('Europe/London').now.to_date,
|
|
37
|
+
to: from + 13
|
|
38
|
+
)
|
|
39
|
+
raise ArgumentError, 'to must not precede from' if to < from
|
|
40
|
+
|
|
41
|
+
cinema = Cinema.new(cinema_id)
|
|
42
|
+
website = LightUk::Internal::Website.new
|
|
43
|
+
films = website.miniguide(cinema.url) + website.coming_soon(cinema.url)
|
|
44
|
+
performances = sessions_between(films, from, to).map do |film, date, session|
|
|
45
|
+
parsed = LightUk::Internal::Parser::Session.new(session, film: film, date: date, venue_url: cinema.url)
|
|
46
|
+
new(parsed.to_hash.merge(cinema_id: cinema.id, cinema_name: cinema.name))
|
|
47
|
+
end
|
|
48
|
+
performances.sort_by(&:starting_at)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @!method showing_on
|
|
52
|
+
# The date of the screening
|
|
53
|
+
# @return [Date]
|
|
54
|
+
|
|
55
|
+
# @!method starting_at
|
|
56
|
+
# UTC time of the screening
|
|
57
|
+
# @return [Time]
|
|
58
|
+
|
|
59
|
+
# @!method variant
|
|
60
|
+
# The kinds of screening (arts, audio_described, kids, subtitled ...)
|
|
61
|
+
# @return <Array[String]>
|
|
62
|
+
|
|
63
|
+
# private
|
|
64
|
+
|
|
65
|
+
# The coming soon page repeats the sessions of films that open within the
|
|
66
|
+
# guide's fortnight, and sessions that have started have no booking id.
|
|
67
|
+
def self.sessions_between(films, from, to)
|
|
68
|
+
sessions = films.flat_map do |film|
|
|
69
|
+
film.fetch('Dates', [])
|
|
70
|
+
.select { |date| (from..to).cover?(Date.iso8601(date.fetch('Key'))) }
|
|
71
|
+
.flat_map { |date| date.fetch('Sessions', []).map { |session| [film, date, session] } }
|
|
72
|
+
end
|
|
73
|
+
sessions.uniq { |film, date, session| session['BOID'] || [film['ID'], date['Key'], session['Display']] }
|
|
74
|
+
end
|
|
75
|
+
private_class_method :sessions_between
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/light_uk.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'cgi/escape'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
require 'cinebase'
|
|
5
|
+
|
|
6
|
+
require_relative './light_uk/version'
|
|
7
|
+
|
|
8
|
+
require_relative './light_uk/internal/http'
|
|
9
|
+
require_relative './light_uk/internal/website'
|
|
10
|
+
require_relative './light_uk/internal/title_sanitizer'
|
|
11
|
+
|
|
12
|
+
require_relative './light_uk/internal/parser/coming_soon'
|
|
13
|
+
require_relative './light_uk/internal/parser/session'
|
|
14
|
+
|
|
15
|
+
require_relative './light_uk/cinema'
|
|
16
|
+
require_relative './light_uk/performance'
|
|
17
|
+
|
|
18
|
+
# light_uk gem
|
|
19
|
+
module LightUk
|
|
20
|
+
end
|