rio_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,169 @@
1
+ # RioUk
2
+
3
+ A simple gem to pull the cinema and its performances from the [Rio Cinema website](https://riocinema.org.uk) and spit out useful formatted info.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rio_uk'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install rio_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
+ No token or setup is needed. The website runs on Savoy Systems' ticketing.
24
+ Every page of it carries the whole programme as one line of JSON, every event
25
+ on sale with every advertised performance, so one request to the what's on
26
+ page reads it all. There is no lighter feed and no compression; the page is
27
+ about 850 KB.
28
+
29
+ ### Cinema
30
+
31
+ The Rio is one venue, keyed `rio`, the name of its Savoy site. The gem holds
32
+ its details: the address from the website's footer and coordinates from
33
+ OpenStreetMap. Cinema methods make no requests.
34
+
35
+ ``` ruby
36
+ RioUk::Cinema.all
37
+ #=> [<RioUk::Cinema ...>]
38
+
39
+ cinema = RioUk::Cinema.new('rio')
40
+ #=> <RioUk::Cinema ...>
41
+
42
+ cinema.adr
43
+ #=> {
44
+ street_address: '107 Kingsland High Street',
45
+ extended_address: nil,
46
+ locality: 'London',
47
+ region: nil,
48
+ postal_code: 'E8 2PB',
49
+ country_name: 'United Kingdom'
50
+ }
51
+
52
+ cinema.brand
53
+ #=> 'Independent'
54
+
55
+ cinema.name
56
+ #=> 'Rio Cinema'
57
+
58
+ cinema.id
59
+ #=> 'rio'
60
+
61
+ cinema.latitude
62
+ #=> 51.5496199
63
+
64
+ cinema.url
65
+ #=> 'https://riocinema.org.uk'
66
+ ```
67
+
68
+ ### Performances
69
+
70
+ `Performance.at` returns performances for an inclusive range of showing dates,
71
+ defaulting to today in London and the following 13 days. Any range is one
72
+ request.
73
+
74
+ ``` ruby
75
+ RioUk::Performance.at('rio', from: Date.today, to: Date.today + 365)
76
+ #=> [<RioUk::Performance ...>, <RioUk::Performance ...>, ...]
77
+
78
+ performance = RioUk::Performance.at('rio').first
79
+ #=> <RioUk::Performance ...>
80
+
81
+ performance.film_name
82
+ #=> 'Dao' # from "Black History Month: Alain Gomis' DAO"
83
+
84
+ performance.film_year
85
+ #=> nil
86
+
87
+ performance.dimension
88
+ #=> '2d'
89
+
90
+ performance.variant
91
+ #=> ['q&a']
92
+
93
+ performance.starting_at
94
+ #=> 2026-11-01 13:30:00 UTC
95
+
96
+ performance.booking_url
97
+ #=> 'https://riocinema.org.uk/Rio.dll/Booking?Booking=TSelectItems.waSelectItemsPrompt.TcsWebMenuItem_0.TcsWebTab_0.TcsPerformance_2884892.TcsSection_1299'
98
+
99
+ performance.cinema_name
100
+ #=> 'Rio Cinema'
101
+
102
+ performance.cinema_id
103
+ #=> 'rio'
104
+ ```
105
+
106
+ Booking links open Savoy's ticket selection for the performance. A few special
107
+ events sell through a partner, such as the Japanese Film Club, and link there.
108
+ Sold out performances are listed with no booking link.
109
+
110
+ Variants come from the Rio's performance flags and from titles: `baby` (Carers
111
+ + Baby), `senior` (Classic Matinee, for the over 60s), `kids` (Family Flicks
112
+ and Hackney Children's Film Fest), `subtitled` (Hard of Hearing), `q&a`,
113
+ `relaxed`, `35mm`, `70mm` and `sing_along`. Pink Palace, Rio Forever, special
114
+ event and no ads flags are not variants.
115
+
116
+ Film years come from the listing's year, or a year in the title. Double bills
117
+ ('1974 / 1976') and programmes ('Various') have none.
118
+
119
+ Only films are kept: events whose type names a film or feature and that have a
120
+ director or a year. Gigs and parties listed as films, such as 'Velcro Flats
121
+ presents POZI', have neither.
122
+
123
+ Titles are cleaned up. The Rio writes the film in capitals and everything else
124
+ in mixed case:
125
+
126
+ - Strands, clubs, festivals and presenters before a colon go: 'Pink Palace: A
127
+ SINGLE MAN', 'JAPANESE FILM CLUB: YOUR NAME', 'Loose Willis presents: THE
128
+ DEVILS'.
129
+ - A mixed case prefix before a film in capitals goes: 'Category H: MALATESTA’S
130
+ CARNIVAL OF BLOOD + FINAL DESTINATION 3'.
131
+ - Mixed case extras after a film in capitals go: 'PAN'S LABYRINTH on 35mm',
132
+ 'WE, THE HATED (Just Stop Oil doc) + Q&A', '... with Smut Press'.
133
+ - A director's name in front goes: "Alain Gomis' DAO" becomes 'Dao'.
134
+ - Capitals become title case: 'SHAUN THE SHEEP: THE BEAST OF MOSSY BOTTOM'
135
+ becomes 'Shaun the Sheep: The Beast of Mossy Bottom'. Words with digits,
136
+ Roman numerals and words without vowels, such as 'M3GAN', 'II' and 'RRR',
137
+ keep their capitals.
138
+ - Event cinema names stay and are standardised: 'NT Live: HAMLET' becomes
139
+ 'National Theatre Live: Hamlet', 'RBO 2026-27: SWAN LAKE' becomes 'Royal
140
+ Ballet and Opera: Swan Lake'.
141
+
142
+ Double bills keep both films: 'Flesh Gordon + Gums'.
143
+
144
+ ## Tests
145
+
146
+ ```sh
147
+ bundle exec rake # unit tests against fixtures
148
+ LIVE=1 bundle exec rake live # live riocinema.org.uk
149
+ ```
150
+
151
+ The fixture in `test/fixtures` is the what's on page, trimmed to its programme
152
+ of a few events with short synopses. `test/support/api_shapes.rb` describes
153
+ the JSON the gem reads. The unit suite checks the fixture against it and the
154
+ live suite checks the real page, held only in memory. When the page changes,
155
+ update the shape first, then the fixture.
156
+
157
+ ## Releasing
158
+
159
+ Bump the version, merge, then run the **Release Gem** workflow in GitHub
160
+ Actions. It runs the tests, tags the version and publishes the gem through
161
+ RubyGems trusted publishing.
162
+
163
+ ## Contributing
164
+
165
+ 1. Fork it
166
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
167
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
168
+ 4. Push to the branch (`git push origin my-new-feature`)
169
+ 5. Create new Pull Request
@@ -0,0 +1,99 @@
1
+ module RioUk
2
+ # The object representing the Rio Cinema in Dalston, London
3
+ class Cinema < Cinebase::Cinema
4
+ # @!attribute [r] id
5
+ # @return [String] the cinema's id, 'rio'
6
+
7
+ # The Rio is one venue. Its id is the name of its Savoy site, as in
8
+ # riocinema.org.uk/Rio.dll.
9
+ VENUES = {
10
+ 'rio' => {
11
+ name: 'Rio Cinema', street_address: '107 Kingsland High Street', locality: 'London',
12
+ postal_code: 'E8 2PB', latitude: 51.5496199, longitude: -0.0755805
13
+ }
14
+ }.freeze
15
+
16
+ ADDRESS_PARTS = %i(street_address extended_address locality region postal_code).freeze
17
+ URL = 'https://riocinema.org.uk'.freeze
18
+
19
+ # Constructor
20
+ # @param [String] id the cinema's id, 'rio' or 'Rio'
21
+ # @return [RioUk::Cinema]
22
+ def initialize(id)
23
+ @id = id.to_s.downcase
24
+ end
25
+
26
+ # Return basic cinema information for all cinemas
27
+ # @return [Array<RioUk::Cinema>] the one cinema
28
+ # @example
29
+ # RioUk::Cinema.all
30
+ # #=> [<RioUk::Cinema>]
31
+ def self.all
32
+ VENUES.keys.map { |id| new(id) }
33
+ end
34
+
35
+ # @!method address
36
+ # Address of the cinema
37
+ # @return [Hash] of different address parts
38
+ # @see #adr
39
+
40
+ # Address of the cinema
41
+ # @return [Hash] contains :street_address, :extended_address,
42
+ # :locality, :region, :postal_code, :country_name
43
+ # @example
44
+ # cinema = RioUk::Cinema.new('rio')
45
+ # cinema.adr
46
+ # #=> {
47
+ # street_address: '107 Kingsland High Street',
48
+ # extended_address: nil,
49
+ # locality: 'London',
50
+ # region: nil,
51
+ # postal_code: 'E8 2PB',
52
+ # country_name: 'United Kingdom'
53
+ # }
54
+ # @note Uses method naming as at http://microformats.org/wiki/adr
55
+ def adr
56
+ ADDRESS_PARTS.to_h { |part| [part, venue[part]] }.merge(country_name: 'United Kingdom')
57
+ end
58
+
59
+ # Brand of the cinema
60
+ # @return [String] which will always be 'Independent'
61
+ def brand
62
+ 'Independent'.freeze
63
+ end
64
+
65
+ # @return [Float, nil]
66
+ def latitude
67
+ venue[:latitude]
68
+ end
69
+
70
+ # @return [Float, nil]
71
+ def longitude
72
+ venue[:longitude]
73
+ end
74
+
75
+ # The name of the cinema
76
+ # @return [String]
77
+ # @example
78
+ # RioUk::Cinema.new('rio').name
79
+ # #=> 'Rio Cinema'
80
+ def name
81
+ venue[:name].to_s
82
+ end
83
+
84
+ # The url of the cinema's website
85
+ # @return [String]
86
+ # @example
87
+ # RioUk::Cinema.new('rio').url
88
+ # #=> 'https://riocinema.org.uk'
89
+ def url
90
+ venue.empty? ? '' : URL
91
+ end
92
+
93
+ private
94
+
95
+ def venue
96
+ VENUES.fetch(id, {})
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module RioUk
6
+ # Internal utility classes: Do not use
7
+ # @api private
8
+ module Internal
9
+ # Requests to riocinema.org.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 "Rio Cinema 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,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RioUk
4
+ # @api private
5
+ module Internal
6
+ module Parser
7
+ # Reads one Savoy event, a film or other listing with its performances
8
+ class Event
9
+ # TypeDescription of films, e.g. 'Film' or 'Main Feature'. Other
10
+ # types are live events.
11
+ FILM_TYPE = /\b(?:film|feature)s?\b/i
12
+
13
+ # The Rio's performance flags and the variants they mean. PP (Pink
14
+ # Palace), RF (Rio Forever), SP (special event) and NoAds are strands
15
+ # and promises, not variants.
16
+ FLAGS = {
17
+ 'CB' => 'baby', # Carers + Baby
18
+ 'CM' => 'senior', # Classic Matinee, for the over 60s
19
+ 'FF' => 'kids', # Family Flicks
20
+ 'HC' => 'kids', # Hackney Children's Film Fest
21
+ 'HoH' => 'subtitled', # Hard of Hearing
22
+ 'QA' => 'q&a',
23
+ 'RS' => 'relaxed'
24
+ }.freeze
25
+
26
+ TITLE_VARIANTS = {
27
+ /\b35mm\b/i => '35mm',
28
+ /\b70mm\b/i => '70mm',
29
+ /\bQ\s?[&+]\s?A\b/i => 'q&a',
30
+ /\bSing[- ]?A[- ]?Long\b/i => 'sing_along'
31
+ }.freeze
32
+
33
+ YES = 'Y'
34
+ THREE_D = /\b3D\b/i
35
+ YEAR = /\A\d{4}\z/
36
+ TIME_ZONE = 'Europe/London'
37
+
38
+ # @param [Hash] event one of the website's Savoy events
39
+ def initialize(event)
40
+ @event = event
41
+ end
42
+
43
+ # Films have a director or a year. Gigs and parties listed as films
44
+ # have neither.
45
+ # @return [Boolean]
46
+ def film?
47
+ @event['TypeDescription'].to_s.match?(FILM_TYPE) && !(director.empty? && year_field.empty?)
48
+ end
49
+
50
+ # @param [Range<Date>] dates listings dates to keep
51
+ # @return [Array<Hash>] options for RioUk::Performance.new
52
+ def performances(dates)
53
+ @event.fetch('Performances', []).filter_map do |performance|
54
+ date = Date.iso8601(performance.fetch('StartDate'))
55
+ attributes(performance, date) if dates.cover?(date)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def attributes(performance, date)
62
+ starting_at = starting_at(performance, date)
63
+ {
64
+ booking_url: booking_url(performance),
65
+ dimension: [title, format].any? { |text| text.match?(THREE_D) } ? '3d' : '2d',
66
+ festival: Cinebase::Festival.from_title(title, year: starting_at.year),
67
+ film_name: film_name,
68
+ film_year: film_year,
69
+ starting_at: starting_at,
70
+ variant: variant(performance)
71
+ }
72
+ end
73
+
74
+ def booking_url(performance)
75
+ if performance['IsSoldOut'] == YES || !performance['IsOpenForSale']
76
+ nil
77
+ else
78
+ Website.url(performance.fetch('URL'))
79
+ end
80
+ end
81
+
82
+ def director
83
+ @event['Director'].to_s.strip
84
+ end
85
+
86
+ def film_name
87
+ @film_name ||= TitleSanitizer.new(title, director: director, strands: strands).sanitized
88
+ end
89
+
90
+ # Double bills give two years, e.g. '1974 / 1976', and programmes of
91
+ # shorts 'Various'
92
+ def film_year
93
+ year_field.match?(YEAR) ? year_field.to_i : Cinebase::TitleSanitizer.film_year(title)
94
+ end
95
+
96
+ def format
97
+ @event.fetch('Tags', []).map { |tag| tag['Format'].to_s }.join(' ')
98
+ end
99
+
100
+ # Times are London time, e.g. '1830'
101
+ def starting_at(performance, date)
102
+ hour, minute = performance.fetch('StartTime').unpack('a2a2').map(&:to_i)
103
+ local = Time.utc(date.year, date.month, date.day, hour, minute)
104
+ TZInfo::Timezone.get(TIME_ZONE).local_to_utc(local) { |periods| periods.first }.utc
105
+ end
106
+
107
+ def strands
108
+ @event.fetch('Seasons', []).map { |season| CGI.unescapeHTML(season['SeasonName'].to_s) }
109
+ end
110
+
111
+ def title
112
+ @title ||= CGI.unescapeHTML(@event.fetch('Title')).squeeze(' ').strip
113
+ end
114
+
115
+ def variant(performance)
116
+ from_flags = FLAGS.filter_map { |flag, variant| variant if performance[flag] == YES }
117
+ from_title = TITLE_VARIANTS.filter_map { |pattern, variant| variant if title.match?(pattern) }
118
+ (from_flags + from_title).uniq
119
+ end
120
+
121
+ def year_field
122
+ @event['Year'].to_s.strip
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,116 @@
1
+ module RioUk
2
+ # Internal utility classes: Do not use
3
+ # @api private
4
+ module Internal
5
+ # Sanitize and standardize film titles. The Rio writes the film in
6
+ # capitals and its strand, presenter and extras in mixed case, e.g.
7
+ # 'Little White Lies: PAN'S LABYRINTH on 35mm'.
8
+ class TitleSanitizer < Cinebase::TitleSanitizer
9
+ # Event cinema names to keep in front of the film
10
+ EVENT_CINEMA = /\A(?:NT Live|National Theatre Live|RBO|Royal Ballet|Met Opera|Exhibition on Screen)\b[^:]*:\s*/i
11
+ # A strand or presenter that names a club, festival or presenter
12
+ PRESENTER = /\b(?:club|fest|festival|kino|presents?)\b/i
13
+ SMALL_WORDS = %w(a an and as at but by for from in nor of on or the to v vs via with).freeze
14
+ # Lone Roman numerals, e.g. 'II', stay in capitals
15
+ ROMAN = /\A(?=[IVXLC])C{0,3}(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})\z/
16
+ VOWEL = /[AEIOUYÀ-ÖØ-Ý]/
17
+ # The first two-letter word, which says whether the title is in capitals
18
+ FIRST_WORD = /[\p{L}\p{N}'’-]*\p{L}[\p{L}\p{N}'’-]*\p{L}[\p{L}\p{N}'’-]*/
19
+
20
+ # Constructor
21
+ # @param [String] title a film title, without HTML entities
22
+ # @param [String] director the event's director, whose name some
23
+ # titles lead with, e.g. "Alain Gomis' DAO"
24
+ # @param [Array<String>] strands the event's season names, e.g.
25
+ # 'Pink Palace'
26
+ # @return [RioUk::Internal::TitleSanitizer]
27
+ def initialize(title, director: '', strands: [])
28
+ super(title)
29
+ @director = director
30
+ @strands = strands
31
+ end
32
+
33
+ # sanitized and standardized title
34
+ # @return [String] sanitised title
35
+ def sanitized
36
+ @sanitized ||= begin
37
+ listed = without_director(title.squeeze(' ').strip)
38
+ event_cinema = listed[EVENT_CINEMA].to_s
39
+ film = without_strand(listed.delete_prefix(event_cinema))
40
+ film = titlecase(leading_capitals(film)) if capitals?(film)
41
+ film = remove.reduce(event_cinema + film) { |text, pattern| text.gsub(pattern, '') }
42
+ replace.reduce(film) { |text, (pattern, prefix)| text.gsub(pattern) { prefix + $1 } }.squeeze(' ').strip
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # strings and regex to be removed
49
+ def remove
50
+ [/[\s:,+-]+\z/] + COMMON_REMOVE
51
+ end
52
+
53
+ # regexes and their replacements
54
+ def replace
55
+ {
56
+ /\A(?:NT|National Theatre) Live:\s*(.*)/i => 'National Theatre Live: ',
57
+ /\ARBO (?:Cinema Season )?20\d{2}-\d{2}:\s*(.*)/i => 'Royal Ballet and Opera: ',
58
+ /\AExhibition on Screen:\s*(.*)/i => 'Exhibition on Screen: '
59
+ }.merge(COMMON_REPLACE)
60
+ end
61
+
62
+ def capitals?(text)
63
+ word = text[FIRST_WORD]
64
+ !word.nil? && !word.match?(/\p{Ll}/)
65
+ end
66
+
67
+ # Mixed case words after the film are extras, e.g. 'on 35mm'
68
+ def leading_capitals(text)
69
+ text.split(' ').take_while { |word| !word.match?(/\p{Ll}/) }.join(' ')
70
+ end
71
+
72
+ def without_director(text)
73
+ if @director.empty?
74
+ text
75
+ else
76
+ text.sub(/#{Regexp.escape(@director)}['’]s?\s+(?=\S)/i, '')
77
+ end
78
+ end
79
+
80
+ # The first prefix before a colon goes when it is one of the event's
81
+ # strands, names a club, festival or presenter, or is in mixed case in
82
+ # front of a film in capitals
83
+ def without_strand(text)
84
+ prefix, film = text.split(/:\s+/, 2)
85
+ if film.nil?
86
+ text.sub(/\A.+?\s+presents\s+/i, '')
87
+ elsif strand?(prefix) || prefix.match?(PRESENTER) || (prefix.match?(/\p{Ll}/) && capitals?(film))
88
+ film
89
+ else
90
+ text
91
+ end
92
+ end
93
+
94
+ def strand?(prefix)
95
+ @strands.any? { |strand| prefix.downcase.start_with?(strand.downcase) }
96
+ end
97
+
98
+ # Words start with a capital, except small words inside the title
99
+ def titlecase(text)
100
+ text.gsub(/[\p{L}\p{N}'’]+/) do |word|
101
+ titlecase_word(word, first: $`.match?(/\A[^\p{L}\p{N}]*\z|[:(–-]\s*\z/))
102
+ end
103
+ end
104
+
105
+ def titlecase_word(word, first:)
106
+ if !first && SMALL_WORDS.include?(word.downcase)
107
+ word.downcase
108
+ elsif word.match?(/\p{N}|\p{Ll}/) || word.length < 2 || word.match?(ROMAN) || !word.match?(VOWEL)
109
+ word
110
+ else
111
+ word.capitalize
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RioUk
4
+ # Internal utility classes: Do not use
5
+ # @api private
6
+ module Internal
7
+ # Reads the Rio Cinema's website, run on Savoy Systems' ticketing. No
8
+ # token is needed.
9
+ class Website
10
+ include Http
11
+
12
+ WHATS_ON = 'https://riocinema.org.uk/Rio.dll/WhatsOn'
13
+ # Savoy pages carry the whole programme as one line of JSON
14
+ EVENTS = /^var Events =\s*(\{.*\})\s*$/
15
+
16
+ # Every event on sale with every advertised performance, however far
17
+ # ahead, including sold out ones
18
+ # @return [Array<Hash>] Savoy events, each with its 'Performances'
19
+ def events
20
+ JSON.parse(get(WHATS_ON)[EVENTS, 1] || '{}').fetch('Events', [])
21
+ end
22
+
23
+ # @param [String] path a Savoy link relative to the what's on page,
24
+ # e.g. 'Booking?Booking=...'
25
+ # @return [String] the absolute url
26
+ def self.url(path)
27
+ URI.join(WHATS_ON, path).to_s
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,66 @@
1
+ module RioUk
2
+ # A single screening of a film on the Rio Cinema website
3
+ class Performance < Cinebase::Performance
4
+ # @!attribute [r] booking_url
5
+ # @return [String, nil] the booking URL, nil when sold out
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 of the film
24
+ # @option options [Time] :starting_at listed start time of the performance
25
+
26
+ TIME_ZONE = 'Europe/London'.freeze
27
+
28
+ # Performances within an inclusive range of showing dates, from one
29
+ # request: the what's on page holds every advertised date.
30
+ # @param [String] cinema_id the cinema's id, 'rio'
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<RioUk::Performance>]
34
+ def self.at(
35
+ cinema_id,
36
+ from: TZInfo::Timezone.get(TIME_ZONE).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
+ if cinema.name.empty?
43
+ []
44
+ else
45
+ events = RioUk::Internal::Website.new.events.map { |event| RioUk::Internal::Parser::Event.new(event) }
46
+ performances = events.select(&:film?).flat_map { |event| event.performances(from..to) }.map do |attributes|
47
+ new(attributes.merge(cinema_id: cinema.id, cinema_name: cinema.name))
48
+ end
49
+ performances.sort_by(&:starting_at)
50
+ end
51
+ end
52
+
53
+ # @!method showing_on
54
+ # The date of the screening
55
+ # @return [Date]
56
+
57
+ # @!method starting_at
58
+ # UTC time of the screening
59
+ # @return [Time]
60
+
61
+ # @!method variant
62
+ # The kinds of screening (35mm, baby, kids, q&a, relaxed, senior,
63
+ # subtitled ...)
64
+ # @return <Array[String]>
65
+ end
66
+ end
@@ -0,0 +1,6 @@
1
+ # Ruby interface for https://riocinema.org.uk
2
+ # @version 1.0.0
3
+ module RioUk
4
+ # Gem version
5
+ VERSION = '1.0.0'.freeze
6
+ end
data/lib/rio_uk.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'cgi/escape'
2
+ require 'json'
3
+
4
+ require 'cinebase'
5
+
6
+ require_relative './rio_uk/version'
7
+
8
+ require_relative './rio_uk/internal/http'
9
+ require_relative './rio_uk/internal/website'
10
+ require_relative './rio_uk/internal/title_sanitizer'
11
+ require_relative './rio_uk/internal/parser/event'
12
+
13
+ require_relative './rio_uk/cinema'
14
+ require_relative './rio_uk/performance'
15
+
16
+ # rio_uk gem
17
+ module RioUk
18
+ end