depot_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,159 @@
1
+ # DepotUk
2
+
3
+ A simple gem to pull the cinema and performances from the website of
4
+ [Depot](https://lewesdepot.org), the independent cinema in Lewes, and spit
5
+ out useful formatted info.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'depot_uk'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install depot_uk
20
+
21
+ ## Usage
22
+
23
+ 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.
24
+
25
+ Performance titles are sanitized as much as possible, removing the parent
26
+ and baby, Knitflicks and See It First prefixes, the Cinecity suffix and
27
+ opera and ballet labels, and naming Royal Ballet and Opera screenings in
28
+ full.
29
+
30
+ No token or setup is needed. The website runs WordPress with Jacro's
31
+ plugin, and tickets are sold through Jacro's booking site.
32
+
33
+ ### Cinema
34
+
35
+ Depot is one venue, so the gem holds it: the address from the website's
36
+ travel page and the coordinates from OpenStreetMap. Its id is `'deplew'`,
37
+ the Jacro code in the website's booking links. Cinema methods make no
38
+ requests.
39
+
40
+ ``` ruby
41
+ DepotUk::Cinema.all
42
+ #=> [<DepotUk::Cinema ...>]
43
+
44
+ cinema = DepotUk::Cinema.new('deplew')
45
+ #=> <DepotUk::Cinema ...>
46
+
47
+ cinema.adr
48
+ #=> {
49
+ street_address: 'Pinwell Road',
50
+ extended_address: nil,
51
+ locality: 'Lewes',
52
+ region: 'East Sussex',
53
+ postal_code: 'BN7 2JS',
54
+ country_name: 'United Kingdom'
55
+ }
56
+
57
+ cinema.brand
58
+ #=> 'Independent'
59
+
60
+ cinema.name
61
+ #=> 'Depot'
62
+
63
+ cinema.latitude
64
+ #=> 50.8715417
65
+
66
+ cinema.url
67
+ #=> 'https://lewesdepot.org'
68
+ ```
69
+
70
+ ### Performances
71
+
72
+ `Performance.at` returns performances for an inclusive range of showing dates,
73
+ defaulting to today in London and the following 13 days.
74
+
75
+ The website's calendar holds every advertised date, so any range takes five
76
+ requests: the calendar, the home page for the full titles the calendar
77
+ shortens, and the descriptive subtitles, relaxed and autism friendly access
78
+ pages. A range with no performances takes one. The site is slow: expect
79
+ about five seconds.
80
+
81
+ ``` ruby
82
+ DepotUk::Performance.at('deplew', from: Date.today, to: Date.today + 2)
83
+ #=> [<DepotUk::Performance ...>, <DepotUk::Performance ...>, ...]
84
+
85
+ performance = DepotUk::Performance.at('deplew').first
86
+ #=> <DepotUk::Performance ...>
87
+
88
+ performance.film_name
89
+ #=> 'Wicker' # from 'Parent & Baby ONLY: Wicker'
90
+
91
+ performance.film_year
92
+ #=> nil # from the title, e.g. 1990 for 'Misery (1990)'
93
+
94
+ performance.dimension
95
+ #=> '2d'
96
+
97
+ performance.variant
98
+ #=> ['baby', 'relaxed', 'subtitled']
99
+
100
+ performance.subtitled?
101
+ #=> true
102
+
103
+ performance.festival
104
+ #=> nil # e.g. #<data Cinebase::Festival name="Cinecity", year=2026>
105
+
106
+ performance.starting_at
107
+ #=> 2026-11-02 11:00:00 UTC
108
+
109
+ performance.booking_url
110
+ #=> 'https://lewesdepot.org/film/parent-baby-only-wicker/booknow/40631087'
111
+
112
+ performance.cinema_name
113
+ #=> 'Depot'
114
+
115
+ performance.cinema_id
116
+ #=> 'deplew'
117
+ ```
118
+
119
+ Booking links are the website's own, to the page that holds Jacro's seat
120
+ selection.
121
+
122
+ Only films are returned. Films play in the numbered screens, so the gem
123
+ drops listings marked 'Depot event' or held in the studio: talks,
124
+ workshops, courses and tastings.
125
+
126
+ Variants come from the access pages and from titles: `subtitled`
127
+ (descriptive subtitles), `relaxed` (relaxed and autism friendly), `baby`
128
+ (parent and baby), `kids` (CBeebies and sensory friendly), `arts` (Royal
129
+ Ballet and Opera, National Theatre Live, Exhibition on Screen, musicals,
130
+ plays and pantomimes) and `q&a`. Films subtitled from another language are
131
+ not a variant.
132
+
133
+ ## Tests
134
+
135
+ ```sh
136
+ bundle exec rake # unit tests against fixtures
137
+ bundle exec rake live # live lewesdepot.org
138
+ ```
139
+
140
+ The fixtures in `test/fixtures` are the website's pages, trimmed to the
141
+ parts the gem reads: a few calendar days, the home page's film list and the
142
+ access pages' listings. `test/support/api_shapes.rb` describes those parts.
143
+ The unit suite checks the fixtures against it and the live suite checks the
144
+ real pages, held only in memory. When the website changes, update the shape
145
+ first, then the fixtures.
146
+
147
+ ## Releasing
148
+
149
+ Bump the version, merge, then run the **Release Gem** workflow in GitHub
150
+ Actions. It runs the tests, tags the version and publishes the gem through
151
+ RubyGems trusted publishing.
152
+
153
+ ## Contributing
154
+
155
+ 1. Fork it
156
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
157
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
158
+ 4. Push to the branch (`git push origin my-new-feature`)
159
+ 5. Create new Pull Request
@@ -0,0 +1,100 @@
1
+ module DepotUk
2
+ # The object representing the Depot cinema in Lewes
3
+ class Cinema < Cinebase::Cinema
4
+ # @!attribute [r] id
5
+ # @return [String] the cinema's Jacro code, 'deplew'
6
+
7
+ # Depot is one venue. Its id is the Jacro code in the website's booking
8
+ # links. The address is from the website's travel page, the coordinates
9
+ # from the cinema's OpenStreetMap node 44767219.
10
+ VENUES = {
11
+ 'deplew' => {
12
+ name: 'Depot', street_address: 'Pinwell Road', locality: 'Lewes', region: 'East Sussex',
13
+ postal_code: 'BN7 2JS', latitude: 50.8715417, longitude: 0.0120094
14
+ }
15
+ }.freeze
16
+
17
+ ADDRESS_PARTS = %i(street_address extended_address locality region postal_code).freeze
18
+ URL = 'https://lewesdepot.org'.freeze
19
+
20
+ # Constructor
21
+ # @param [String] id the cinema's Jacro code, 'deplew' or 'DEPLEW'
22
+ # @return [DepotUk::Cinema]
23
+ def initialize(id)
24
+ @id = id.to_s.downcase
25
+ end
26
+
27
+ # Return basic cinema information for all cinemas
28
+ # @return [Array<DepotUk::Cinema>]
29
+ # @example
30
+ # DepotUk::Cinema.all
31
+ # #=> [<DepotUk::Cinema>]
32
+ def self.all
33
+ VENUES.keys.map { |id| new(id) }
34
+ end
35
+
36
+ # @!method address
37
+ # Address of the cinema
38
+ # @return [Hash] of different address parts
39
+ # @see #adr
40
+
41
+ # Address of the cinema
42
+ # @return [Hash] contains :street_address, :extended_address,
43
+ # :locality, :region, :postal_code, :country_name
44
+ # @example
45
+ # cinema = DepotUk::Cinema.new('deplew')
46
+ # cinema.adr
47
+ # #=> {
48
+ # street_address: 'Pinwell Road',
49
+ # extended_address: nil,
50
+ # locality: 'Lewes',
51
+ # region: 'East Sussex',
52
+ # postal_code: 'BN7 2JS',
53
+ # country_name: 'United Kingdom'
54
+ # }
55
+ # @note Uses method naming as at http://microformats.org/wiki/adr
56
+ def adr
57
+ ADDRESS_PARTS.to_h { |part| [part, venue[part]] }.merge(country_name: 'United Kingdom')
58
+ end
59
+
60
+ # Brand of the cinema
61
+ # @return [String] which will always be 'Independent'
62
+ def brand
63
+ 'Independent'.freeze
64
+ end
65
+
66
+ # @return [Float, nil]
67
+ def latitude
68
+ venue[:latitude]
69
+ end
70
+
71
+ # @return [Float, nil]
72
+ def longitude
73
+ venue[:longitude]
74
+ end
75
+
76
+ # The name of the cinema
77
+ # @return [String]
78
+ # @example
79
+ # DepotUk::Cinema.new('deplew').name
80
+ # #=> 'Depot'
81
+ def name
82
+ venue[:name].to_s
83
+ end
84
+
85
+ # The url of the cinema's website
86
+ # @return [String]
87
+ # @example
88
+ # DepotUk::Cinema.new('deplew').url
89
+ # #=> 'https://lewesdepot.org'
90
+ def url
91
+ venue.empty? ? '' : URL
92
+ end
93
+
94
+ private
95
+
96
+ def venue
97
+ VENUES.fetch(id, {})
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module DepotUk
6
+ # Internal utility classes: Do not use
7
+ # @api private
8
+ module Internal
9
+ # Requests to lewesdepot.org over one kept-alive connection. The site is
10
+ # slow, so timeouts are long.
11
+ module Http
12
+ HOST = 'lewesdepot.org'
13
+ TIMEOUT = 40
14
+ RETRIES = 2
15
+ USER_AGENT = 'Mozilla/5.0 (compatible; depot_uk; +https://github.com/andycroll/depot_uk)'
16
+ TRANSIENT_ERRORS = [
17
+ EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
18
+ ].freeze
19
+
20
+ # Close the connection
21
+ # @return [nil]
22
+ def close
23
+ @connection.finish if @connection&.started?
24
+ @connection = nil
25
+ end
26
+
27
+ private
28
+
29
+ def get(path)
30
+ attempts = 0
31
+ begin
32
+ response = connection.request(Net::HTTP::Get.new(path, 'User-Agent' => USER_AGENT))
33
+ rescue *TRANSIENT_ERRORS
34
+ close
35
+ attempts += 1
36
+ raise if attempts > RETRIES
37
+
38
+ sleep(attempts)
39
+ retry
40
+ end
41
+ # Omit response bodies from errors, and never follow redirects.
42
+ raise "Depot HTTP error #{response.code}" unless response.is_a?(Net::HTTPSuccess)
43
+
44
+ String.new(response.body, encoding: Encoding::UTF_8)
45
+ end
46
+
47
+ def connection
48
+ @connection ||= Net::HTTP.start(HOST, 443, use_ssl: true, open_timeout: TIMEOUT, read_timeout: TIMEOUT)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepotUk
4
+ # @api private
5
+ module Internal
6
+ module Parser
7
+ # Reads the website's calendar: every advertised date, each with its
8
+ # listings and their start times. Dates have no year.
9
+ class Calendar
10
+ # One performance of a film
11
+ Entry = Data.define(:id, :slug, :heading, :date, :starting_at)
12
+
13
+ DAY = %r{<h2>(\w{3})<br>(\d{1,2}) (\w+)</h2>}
14
+ LISTING = %r{
15
+ data-link="/film/([^"]+)"\s*><div\sclass="contents"><h3>(.*?)</h3>
16
+ (.*?)<div\sclass="calendar-perf-buttons">(.*?)</div>
17
+ }mx
18
+ PERFORMANCE = %r{perf-popup\.php\?id=(\d+)"[^>]*>\s*(\d{1,2}):(\d{2})\s*</a>}
19
+ LINE_BREAK = %r{<br\s*/?>}
20
+
21
+ # Films play in numbered screens. Talks, workshops and tastings are
22
+ # 'Depot event' or in the studio.
23
+ FILM_SCREEN = /\AScreen \d/
24
+
25
+ # A day and month falls on a different weekday in each of three
26
+ # neighbouring years
27
+ YEARS_AROUND = (-1..1)
28
+ TIME_ZONE = 'Europe/London'
29
+
30
+ # @param [String] html the calendar page
31
+ # @param [Date] near a date within a year of the listings, which sets
32
+ # their years
33
+ def initialize(html, near:)
34
+ @html = html
35
+ @near = near
36
+ end
37
+
38
+ # @return [Array<DepotUk::Internal::Parser::Calendar::Entry>] film
39
+ # performances in calendar order
40
+ def entries
41
+ date = nil
42
+ @html.scan(Regexp.union(DAY, LISTING)).flat_map do |weekday, day, month, slug, heading, details, buttons|
43
+ if weekday
44
+ date = date_of(weekday, day.to_i, month)
45
+ []
46
+ elsif date && film?(details)
47
+ performances(buttons, slug: slug, heading: CGI.unescapeHTML(heading).strip, date: date)
48
+ else
49
+ []
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def date_of(weekday, day, month_name)
57
+ month = Date::MONTHNAMES.index(month_name)
58
+ if month
59
+ YEARS_AROUND.filter_map do |offset|
60
+ year = @near.year + offset
61
+ Date.new(year, month, day) if Date.valid_date?(year, month, day)
62
+ end.find { |date| date.strftime('%a') == weekday }
63
+ end
64
+ end
65
+
66
+ def film?(details)
67
+ details.split(LINE_BREAK).any? { |line| line.strip.match?(FILM_SCREEN) }
68
+ end
69
+
70
+ def performances(buttons, slug:, heading:, date:)
71
+ buttons.scan(PERFORMANCE).map do |id, hour, minute|
72
+ local = Time.utc(date.year, date.month, date.day, hour.to_i, minute.to_i)
73
+ starting_at = TZInfo::Timezone.get(TIME_ZONE).local_to_utc(local)
74
+ Entry.new(id: id, slug: slug, heading: heading, date: date, starting_at: starting_at)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepotUk
4
+ # @api private
5
+ module Internal
6
+ module Parser
7
+ # Turns one calendar entry into performance attributes
8
+ class Showtime
9
+ BOOKING_URL = 'https://lewesdepot.org/film/%<slug>s/booknow/%<id>s'
10
+
11
+ # The calendar cuts long headings at a word and adds this
12
+ TRUNCATION = '...'
13
+
14
+ # Titles and the variants they mean. The access pages mark
15
+ # subtitled and relaxed performances.
16
+ TITLE_VARIANTS = {
17
+ /\AParent (?:&|and) Baby\b/i => 'baby',
18
+ /\ASensory Friendly\b/i => %w(relaxed kids),
19
+ /\ACbeebies\b/i => 'kids',
20
+ /\bQ\s?&\s?A\b/i => 'q&a',
21
+ /\A(?:RBO|National Theatre Live|NT Live|Met Opera|Exhibition on Screen):/i => 'arts',
22
+ /\bThe (?:Musical|Play)\z|\bLive On Stage\z|\bPanto\b/i => 'arts'
23
+ }.freeze
24
+
25
+ CINECITY = /\(Cinecity\)/i
26
+ CINECITY_NAME = 'Cinecity'
27
+ THREE_D = /\b3D\b/i
28
+
29
+ # @param [DepotUk::Internal::Parser::Calendar::Entry] entry
30
+ # @param [Array<String>] titles every advertised title in full
31
+ # @param [Array<String>] variants from the access pages
32
+ def initialize(entry, titles:, variants:)
33
+ @entry = entry
34
+ @titles = titles
35
+ @variants = variants
36
+ end
37
+
38
+ # @return [Hash] options for DepotUk::Performance.new
39
+ def to_hash
40
+ {
41
+ booking_url: format(BOOKING_URL, slug: @entry.slug, id: @entry.id),
42
+ dimension: title.match?(THREE_D) ? '3d' : '2d',
43
+ festival: festival,
44
+ film_name: TitleSanitizer.new(title).sanitized,
45
+ film_year: Cinebase::TitleSanitizer.film_year(title),
46
+ starting_at: @entry.starting_at,
47
+ variant: variant
48
+ }
49
+ end
50
+
51
+ # WordPress's slug for a title, e.g. 'de-gaulle-liberte'
52
+ # @param [String] title
53
+ # @return [String]
54
+ def self.slug(title)
55
+ ascii = title.unicode_normalize(:nfkd).gsub(/\p{Mn}/, '').downcase.delete("'’")
56
+ ascii.gsub(/[^a-z0-9]+/, '-').delete_prefix('-').delete_suffix('-')
57
+ end
58
+
59
+ private
60
+
61
+ def festival
62
+ year = @entry.date.year
63
+ if title.match?(CINECITY)
64
+ Cinebase::Festival.new(name: CINECITY_NAME, year: year)
65
+ else
66
+ Cinebase::Festival.from_title(title, year: year)
67
+ end
68
+ end
69
+
70
+ def title
71
+ @title ||= if @entry.heading.end_with?(TRUNCATION)
72
+ full_title(@entry.heading.delete_suffix(TRUNCATION))
73
+ else
74
+ @entry.heading
75
+ end
76
+ end
77
+
78
+ # The film's slug settles a shortened heading that starts several
79
+ # titles, e.g. 'Exhibition on Screen:...'
80
+ def full_title(start)
81
+ matches = @titles.select { |candidate| candidate.start_with?(start) }
82
+ if matches.one?
83
+ matches.first
84
+ else
85
+ matches.find { |candidate| self.class.slug(candidate) == @entry.slug } || start
86
+ end
87
+ end
88
+
89
+ def variant
90
+ from_title = TITLE_VARIANTS.flat_map { |pattern, variants| title.match?(pattern) ? Array(variants) : [] }
91
+ (from_title + @variants).uniq
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,37 @@
1
+ module DepotUk
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 [DepotUk::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
+ /\AParent (?:&|and) Baby ONLY:\s*/i, # parent and baby screening
22
+ /\AKNITFLICKS:\s*/i, # knit-along screening
23
+ /\ASee It First\s*[–-]\s*/i, # preview
24
+ /\s*\(Cinecity\)/i, # festival suffix
25
+ /\s*\((?:MET )?(?:Opera|Ballet)\)\z/i # event cinema genre
26
+ ] + COMMON_REMOVE
27
+ end
28
+
29
+ # regexes and their replacements
30
+ def replace
31
+ {
32
+ /\ARBO: (.*)/ => 'Royal Ballet and Opera: '
33
+ }.merge(COMMON_REPLACE)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepotUk
4
+ # Internal utility classes: Do not use
5
+ # @api private
6
+ module Internal
7
+ # Reads the pages of lewesdepot.org that, together, describe every
8
+ # advertised performance: the calendar, the home page's film list and
9
+ # the access pages.
10
+ class Website
11
+ include Http
12
+
13
+ CALENDAR_PATH = '/whats-on/calendar'
14
+ HOME_PATH = '/'
15
+
16
+ # Each access page lists every upcoming performance of its kind
17
+ ACCESS_PAGES = {
18
+ '/access/descriptive-subtitles' => 'subtitled',
19
+ '/access/relaxed' => 'relaxed',
20
+ '/access/autism-friendly' => 'relaxed'
21
+ }.freeze
22
+
23
+ FILM_SELECT = %r{<select[^>]*id="film_select"[^>]*>(.*?)</select>}m
24
+ FILM_OPTION = %r{<option value="\d+">([^<]+)</option>}
25
+ PERFORMANCE_ID = /perf-popup\.php\?id=(\d+)/
26
+
27
+ # Yield a website, closing its connection afterwards
28
+ # @yieldparam [DepotUk::Internal::Website] website
29
+ # @return the block's result
30
+ def self.open
31
+ website = new
32
+ yield website
33
+ ensure
34
+ website.close
35
+ end
36
+
37
+ # @return [String] the calendar page, every advertised date
38
+ def calendar
39
+ get(CALENDAR_PATH)
40
+ end
41
+
42
+ # The calendar shortens long titles, the home page's film list does
43
+ # not
44
+ # @return [Array<String>] every advertised title, e.g. 'Ghosts: The
45
+ # Possession of Button House'
46
+ def titles
47
+ films = get(HOME_PATH)[FILM_SELECT, 1].to_s
48
+ films.scan(FILM_OPTION).map { |(title)| CGI.unescapeHTML(title).strip }
49
+ end
50
+
51
+ # @return [Hash{String => Array<String>}] variants by performance id,
52
+ # e.g. { '40631087' => ['subtitled', 'relaxed'] }
53
+ def variants
54
+ ACCESS_PAGES.each_with_object({}) do |(path, variant), variants|
55
+ get(path).scan(PERFORMANCE_ID).each do |(id)|
56
+ variants[id] = (variants[id] || []) | [variant]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,80 @@
1
+ module DepotUk
2
+ # A single screening of a film at the Depot cinema
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 [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. The calendar
29
+ # holds every advertised date, so any range takes five requests: the
30
+ # calendar, the home page for titles the calendar shortens, and three
31
+ # access pages. A range with no performances takes one.
32
+ # @param [String] cinema_id the cinema's Jacro code, 'deplew'
33
+ # @param [Date] from first date (defaults to today in London)
34
+ # @param [Date] to last date (defaults to 13 days after from)
35
+ # @return [Array<DepotUk::Performance>]
36
+ def self.at(
37
+ cinema_id,
38
+ from: TZInfo::Timezone.get(TIME_ZONE).now.to_date,
39
+ to: from + 13
40
+ )
41
+ raise ArgumentError, 'to must not precede from' if to < from
42
+
43
+ cinema = Cinema.new(cinema_id)
44
+ if cinema.name.empty?
45
+ []
46
+ else
47
+ performances = Internal::Website.open { |website| read(website, cinema, from..to) }
48
+ performances.sort_by(&:starting_at)
49
+ end
50
+ end
51
+
52
+ # @!method showing_on
53
+ # The date of the screening
54
+ # @return [Date]
55
+
56
+ # @!method starting_at
57
+ # UTC time of the screening
58
+ # @return [Time]
59
+
60
+ # @!method variant
61
+ # The kinds of screening (arts, baby, kids, q&a, relaxed, subtitled)
62
+ # @return <Array[String]>
63
+
64
+ def self.read(website, cinema, dates)
65
+ entries = Internal::Parser::Calendar.new(website.calendar, near: dates.first).entries
66
+ entries = entries.select { |entry| dates.cover?(entry.date) }
67
+ if entries.empty?
68
+ []
69
+ else
70
+ titles = website.titles
71
+ variants = website.variants
72
+ entries.map do |entry|
73
+ showtime = Internal::Parser::Showtime.new(entry, titles: titles, variants: variants.fetch(entry.id, []))
74
+ new(showtime.to_hash.merge(cinema_id: cinema.id, cinema_name: cinema.name))
75
+ end
76
+ end
77
+ end
78
+ private_class_method :read
79
+ end
80
+ end
@@ -0,0 +1,6 @@
1
+ # Ruby interface for https://lewesdepot.org
2
+ # @version 1.0.0
3
+ module DepotUk
4
+ # Gem version
5
+ VERSION = '1.0.0'.freeze
6
+ end
data/lib/depot_uk.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'cgi/escape'
2
+
3
+ require 'cinebase'
4
+
5
+ require_relative './depot_uk/version'
6
+
7
+ require_relative './depot_uk/internal/http'
8
+ require_relative './depot_uk/internal/website'
9
+ require_relative './depot_uk/internal/title_sanitizer'
10
+ require_relative './depot_uk/internal/parser/calendar'
11
+ require_relative './depot_uk/internal/parser/showtime'
12
+
13
+ require_relative './depot_uk/cinema'
14
+ require_relative './depot_uk/performance'
15
+
16
+ # depot_uk gem
17
+ module DepotUk
18
+ end