odeon_uk 1.1.5 → 2.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 +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +8 -1
- data/README.md +1 -0
- data/Rakefile +13 -9
- data/lib/odeon_uk/cinema.rb +14 -42
- data/lib/odeon_uk/film.rb +23 -5
- data/lib/odeon_uk/internal/film_with_screenings_parser.rb +133 -46
- data/lib/odeon_uk/internal/showtimes_page.rb +36 -0
- data/lib/odeon_uk/internal/title_sanitizer.rb +49 -0
- data/lib/odeon_uk/internal/website.rb +36 -0
- data/lib/odeon_uk/screening.rb +56 -22
- data/lib/odeon_uk/version.rb +2 -2
- data/lib/odeon_uk.rb +4 -2
- data/odeon_uk.gemspec +0 -1
- data/test/fixture_updater.rb +72 -0
- data/test/fixtures/{odeon-bfi-imax.html → cinema/bfi_imax.html} +988 -1130
- data/test/fixtures/{odeon-brighton.html → cinema/brighton.html} +1035 -761
- data/test/fixtures/{odeon-london-leicester-square.html → cinema/leicester_square.html} +470 -766
- data/test/fixtures/showtimes/brighton/film_first.html +92 -0
- data/test/fixtures/showtimes/brighton/film_last.html +100 -0
- data/test/fixtures/showtimes/brighton.html +2071 -0
- data/test/fixtures/showtimes/liverpool_one/film_first_dbox.html +188 -0
- data/test/fixtures/showtimes/manchester/film_first_imax.html +182 -0
- data/test/fixtures/{odeon-sitemap.html → sitemap.html} +572 -444
- data/test/lib/odeon_uk/cinema_test.rb +129 -231
- data/test/lib/odeon_uk/film_test.rb +50 -3
- data/test/lib/odeon_uk/internal/film_with_screenings_parser_test.rb +79 -123
- data/test/lib/odeon_uk/internal/showtimes_page_test.rb +51 -0
- data/test/lib/odeon_uk/internal/title_sanitizer_test.rb +105 -0
- data/test/lib/odeon_uk/internal/website_test.rb +59 -0
- data/test/lib/odeon_uk/screening_test.rb +66 -32
- metadata +32 -53
- data/test/fixtures/brighton-showtimes/about-time.html +0 -175
- data/test/fixtures/brighton-showtimes/autism-friendly-planes.html +0 -75
- data/test/fixtures/brighton-showtimes/bolshoi-spartacus-live.html +0 -67
- data/test/fixtures/brighton-showtimes/cinemagic-echo-planet.html +0 -67
- data/test/fixtures/brighton-showtimes/globe-on-screen-twelfth-night.html +0 -67
- data/test/fixtures/brighton-showtimes/met-opera-eugene-onegin.html +0 -67
- data/test/fixtures/brighton-showtimes/national-theatre-live-frankenstein.html +0 -78
- data/test/fixtures/brighton-showtimes/nt-live-war-horse.html +0 -67
- data/test/fixtures/brighton-showtimes/royal-opera-house-turandot.html +0 -66
- data/test/fixtures/brighton-showtimes/rsc-richard-ii.html +0 -67
- data/test/fixtures/brighton-showtimes/star-trek-into-darkness-2d.html +0 -83
- data/test/fixtures/brighton-showtimes/ukjff-from-cable-street-to-brick-lane.html +0 -67
- data/test/fixtures/manchester-showtimes/thor-the-dark-world.html +0 -300
- data/test/fixtures/odeon-brighton-showtimes.html +0 -1238
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 59c9821b04dd9e8262c25557b82d95b51a9fdf7d
         | 
| 4 | 
            +
              data.tar.gz: b1950e1710b9a4bcda1dbe85611cb26ad3604c48
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: dc7ef841b0748d993a87b43e77cebf818cefa48698a3cd461bb57178f96dc677b5f0014d488d49857e76512912034a5e7c37092dcf1109ee5eb5fddb6d5a81aa
         | 
| 7 | 
            +
              data.tar.gz: 6be57bd2533423a9895ec6f9a4f6aed25c45721886ac244cbed0ab13720f3a31899d045f3d5fd09b936d556d732ece8f5636948b5787565fcb025e28506bc839
         | 
    
        data/.travis.yml
    CHANGED
    
    
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,4 +1,11 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            ## 2.0.0, _22nd September 2014_
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            - add website utility class
         | 
| 4 | 
            +
            - break up film parsing into internal classes
         | 
| 5 | 
            +
            - Film#at builds films not cinema
         | 
| 6 | 
            +
            - fixture update script
         | 
| 7 | 
            +
            - title sanitizer class
         | 
| 8 | 
            +
            - updated fixtures
         | 
| 2 9 | 
             
            - Added changelog
         | 
| 3 10 |  | 
| 4 11 | 
             
            ## 1.1.3, _8th Nov 2013_
         | 
    
        data/README.md
    CHANGED
    
    | @@ -5,6 +5,7 @@ A simple gem to parse the [Odeon UK website](http://odeon.co.uk) and spit out us | |
| 5 5 | 
             
            [](http://badge.fury.io/rb/odeon_uk)
         | 
| 6 6 | 
             
            [](https://codeclimate.com/github/andycroll/odeon_uk)
         | 
| 7 7 | 
             
            [](https://travis-ci.org/andycroll/odeon_uk)
         | 
| 8 | 
            +
            [](http://inch-ci.org/github/andycroll/odeon_uk)
         | 
| 8 9 |  | 
| 9 10 | 
             
            ## Installation
         | 
| 10 11 |  | 
    
        data/Rakefile
    CHANGED
    
    | @@ -1,20 +1,24 @@ | |
| 1 1 | 
             
            #!/usr/bin/env rake
         | 
| 2 | 
            -
            require  | 
| 2 | 
            +
            require 'bundler/gem_tasks'
         | 
| 3 3 |  | 
| 4 4 | 
             
            require 'rake/testtask'
         | 
| 5 5 |  | 
| 6 6 | 
             
            Rake::TestTask.new do |t|
         | 
| 7 7 | 
             
              t.libs << 'lib/odeon_uk'
         | 
| 8 | 
            -
              t.test_files = FileList[ | 
| 8 | 
            +
              t.test_files = FileList[
         | 
| 9 | 
            +
                'test/lib/odeon_uk/*_test.rb',
         | 
| 10 | 
            +
                'test/lib/odeon_uk/internal/*_test.rb'
         | 
| 11 | 
            +
              ]
         | 
| 9 12 | 
             
              t.verbose = true
         | 
| 10 13 | 
             
            end
         | 
| 11 14 |  | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 15 | 
            +
            # http://erniemiller.org/2014/02/05/7-lines-every-gems-rakefile-should-have/
         | 
| 16 | 
            +
            task :console do
         | 
| 17 | 
            +
              require 'irb'
         | 
| 18 | 
            +
              require 'irb/completion'
         | 
| 19 | 
            +
              require 'odeon_uk'
         | 
| 20 | 
            +
              ARGV.clear
         | 
| 21 | 
            +
              IRB.start
         | 
| 14 22 | 
             
            end
         | 
| 15 23 |  | 
| 16 | 
            -
            task : | 
| 17 | 
            -
              system "gem push odeon_uk-#{OdeonUk::VERSION}"
         | 
| 18 | 
            -
            end
         | 
| 19 | 
            -
             | 
| 20 | 
            -
            task :default => :test
         | 
| 24 | 
            +
            task default: :test
         | 
    
        data/lib/odeon_uk/cinema.rb
    CHANGED
    
    | @@ -1,8 +1,6 @@ | |
| 1 1 | 
             
            module OdeonUk
         | 
| 2 | 
            -
             | 
| 3 2 | 
             
              # The object representing a cinema on the Odeon UK website
         | 
| 4 3 | 
             
              class Cinema
         | 
| 5 | 
            -
             | 
| 6 4 | 
             
                # @return [String] the brand of the cinema
         | 
| 7 5 | 
             
                attr_reader :brand
         | 
| 8 6 | 
             
                # @return [Integer] the numeric id of the cinema on the Odeon website
         | 
| @@ -21,16 +19,13 @@ module OdeonUk | |
| 21 19 | 
             
                def initialize(id, name, url)
         | 
| 22 20 | 
             
                  @brand = 'Odeon'
         | 
| 23 21 | 
             
                  @id    = id.to_i
         | 
| 24 | 
            -
                  @name  = name.gsub('London - ','')
         | 
| 25 | 
            -
                  @slug  = @name.downcase.gsub(/[^0-9a-z ]/,'').gsub(/\s+/, '-')
         | 
| 22 | 
            +
                  @name  = name.gsub('London - ', '').gsub(' - ', ': ')
         | 
| 23 | 
            +
                  @slug  = @name.downcase.gsub(/[^0-9a-z ]/, '').gsub(/\s+/, '-')
         | 
| 26 24 | 
             
                  @url   = (url[0] == '/') ? "http://www.odeon.co.uk#{url}" : url
         | 
| 27 25 | 
             
                end
         | 
| 28 26 |  | 
| 29 27 | 
             
                # Return basic cinema information for all cinemas
         | 
| 30 28 | 
             
                # @return [Array<OdeonUk::Cinema>]
         | 
| 31 | 
            -
                # @example
         | 
| 32 | 
            -
                #   OdeonUk::Cinema.all
         | 
| 33 | 
            -
                #   #=> [<OdeonUk::Cinema brand="Odeon" name="Odeon Tunbridge Wells" slug="odeon-tunbridge-wells" id=23 url="...">, #=> <OdeonUk::Cinema brand="Odeon" name="Odeon Brighton" slug="odeon-brighton" chain_id="71" url="...">, ...]
         | 
| 34 29 | 
             
                def self.all
         | 
| 35 30 | 
             
                  cinema_links.map do |link|
         | 
| 36 31 | 
             
                    new_from_link link
         | 
| @@ -38,11 +33,12 @@ module OdeonUk | |
| 38 33 | 
             
                end
         | 
| 39 34 |  | 
| 40 35 | 
             
                # Find a single cinema
         | 
| 41 | 
            -
                # @param [Integer, String] id the cinema id of the format 71/'71' as used on | 
| 36 | 
            +
                # @param [Integer, String] id the cinema id of the format 71/'71' as used on
         | 
| 37 | 
            +
                # the odeon.co.uk website
         | 
| 42 38 | 
             
                # @return [OdeonUk::Cinema, nil]
         | 
| 43 39 | 
             
                # @example
         | 
| 44 40 | 
             
                #   OdeonUk::Cinema.find('71')
         | 
| 45 | 
            -
                #   #=> <OdeonUk::Cinema brand="Odeon" name="Brighton" slug="brighton"  | 
| 41 | 
            +
                #   #=> <OdeonUk::Cinema brand="Odeon" name="Brighton" slug="brighton" ...>
         | 
| 46 42 | 
             
                def self.find(id)
         | 
| 47 43 | 
             
                  id = id.to_i
         | 
| 48 44 | 
             
                  return nil unless id > 0
         | 
| @@ -55,7 +51,10 @@ module OdeonUk | |
| 55 51 | 
             
                # @example
         | 
| 56 52 | 
             
                #   cinema = OdeonUk::Cinema.find('71')
         | 
| 57 53 | 
             
                #   cinema.adr
         | 
| 58 | 
            -
                #   #=> { street_address: 'Kingswest', | 
| 54 | 
            +
                #   #=> { street_address: 'Kingswest',
         | 
| 55 | 
            +
                #         locality: 'Brighton',
         | 
| 56 | 
            +
                #         postal_code: 'BN1 2RE',
         | 
| 57 | 
            +
                #         country_name: 'United Kingdom' }
         | 
| 59 58 | 
             
                def adr
         | 
| 60 59 | 
             
                  {
         | 
| 61 60 | 
             
                    street_address: street_address,
         | 
| @@ -68,15 +67,8 @@ module OdeonUk | |
| 68 67 |  | 
| 69 68 | 
             
                # Films with showings scheduled at this cinema
         | 
| 70 69 | 
             
                # @return [Array<OdeonUk::Film>]
         | 
| 71 | 
            -
                # @example
         | 
| 72 | 
            -
                #   cinema = OdeonUk::Cinema.find('71')
         | 
| 73 | 
            -
                #   cinema.films
         | 
| 74 | 
            -
                #   #=> [<OdeonUk::Film name="Iron Man 3">, <OdeonUk::Film name="Star Trek Into Darkness">]
         | 
| 75 70 | 
             
                def films
         | 
| 76 | 
            -
                   | 
| 77 | 
            -
                    parser = OdeonUk::Internal::FilmWithScreeningsParser.new node.to_s
         | 
| 78 | 
            -
                    OdeonUk::Film.new parser.film_name
         | 
| 79 | 
            -
                  end.uniq
         | 
| 71 | 
            +
                  Film.at(id)
         | 
| 80 72 | 
             
                end
         | 
| 81 73 |  | 
| 82 74 | 
             
                # The name of the cinema including the brand
         | 
| @@ -99,7 +91,6 @@ module OdeonUk | |
| 99 91 | 
             
                  address_node.text.match(/\w+(\s\w+){0,}\s+(\w+(\s\w+){0,})/)[2]
         | 
| 100 92 | 
             
                end
         | 
| 101 93 |  | 
| 102 | 
            -
             | 
| 103 94 | 
             
                # Post code of the cinema
         | 
| 104 95 | 
             
                # @return [String]
         | 
| 105 96 | 
             
                # @example
         | 
| @@ -117,14 +108,7 @@ module OdeonUk | |
| 117 108 | 
             
                #   cinema.screenings
         | 
| 118 109 | 
             
                #   #=> [<OdeonUk::Screening film_name="Iron Man 3" cinema_name="Brighton" when="..." variant="...">, <OdeonUk::Screening ...>]
         | 
| 119 110 | 
             
                def screenings
         | 
| 120 | 
            -
                   | 
| 121 | 
            -
                    parser = OdeonUk::Internal::FilmWithScreeningsParser.new node.to_s
         | 
| 122 | 
            -
                    parser.showings.map do |screening_type, times_urls|
         | 
| 123 | 
            -
                      times_urls.map do |array|
         | 
| 124 | 
            -
                        OdeonUk::Screening.new parser.film_name, self.name, array[0], array[1], screening_type
         | 
| 125 | 
            -
                      end
         | 
| 126 | 
            -
                    end
         | 
| 127 | 
            -
                  end.flatten
         | 
| 111 | 
            +
                  Screening.at(id)
         | 
| 128 112 | 
             
                end
         | 
| 129 113 |  | 
| 130 114 | 
             
                # Screenings for particular film
         | 
| @@ -155,7 +139,7 @@ module OdeonUk | |
| 155 139 | 
             
                private
         | 
| 156 140 |  | 
| 157 141 | 
             
                def self.cinema_links
         | 
| 158 | 
            -
                  links = parsed_sitemap.css('.sitemap  | 
| 142 | 
            +
                  links = parsed_sitemap.css('.sitemap a[href*=cinemas]')
         | 
| 159 143 | 
             
                  links.select { |link| link.get_attribute('href').match(/\/\d+\/$/) }
         | 
| 160 144 | 
             
                end
         | 
| 161 145 |  | 
| @@ -171,7 +155,7 @@ module OdeonUk | |
| 171 155 | 
             
                end
         | 
| 172 156 |  | 
| 173 157 | 
             
                def self.sitemap_response
         | 
| 174 | 
            -
                  @sitemap_response ||=  | 
| 158 | 
            +
                  @sitemap_response ||= OdeonUk::Internal::Website.new.sitemap
         | 
| 175 159 | 
             
                end
         | 
| 176 160 |  | 
| 177 161 | 
             
                def address_node
         | 
| @@ -179,23 +163,11 @@ module OdeonUk | |
| 179 163 | 
             
                end
         | 
| 180 164 |  | 
| 181 165 | 
             
                def cinema_response
         | 
| 182 | 
            -
                  @cinema_response ||=  | 
| 166 | 
            +
                  @cinema_response ||= OdeonUk::Internal::Website.new.cinema(@id)
         | 
| 183 167 | 
             
                end
         | 
| 184 168 |  | 
| 185 169 | 
             
                def parsed_cinema
         | 
| 186 170 | 
             
                  Nokogiri::HTML(cinema_response)
         | 
| 187 171 | 
             
                end
         | 
| 188 | 
            -
             | 
| 189 | 
            -
                def film_nodes
         | 
| 190 | 
            -
                  parsed_showtimes.css('.film-detail')
         | 
| 191 | 
            -
                end
         | 
| 192 | 
            -
             | 
| 193 | 
            -
                def parsed_showtimes
         | 
| 194 | 
            -
                  Nokogiri::HTML(showtimes_response)
         | 
| 195 | 
            -
                end
         | 
| 196 | 
            -
             | 
| 197 | 
            -
                def showtimes_response
         | 
| 198 | 
            -
                  @showtimes_response ||= HTTParty.get("http://www.odeon.co.uk/showtimes/week/#{@id}?siteId=#{@id}")
         | 
| 199 | 
            -
                end
         | 
| 200 172 | 
             
              end
         | 
| 201 173 | 
             
            end
         | 
    
        data/lib/odeon_uk/film.rb
    CHANGED
    
    | @@ -1,5 +1,4 @@ | |
| 1 1 | 
             
            module OdeonUk
         | 
| 2 | 
            -
             | 
| 3 2 | 
             
              # The object representing a film on the Odeon UK website
         | 
| 4 3 | 
             
              class Film
         | 
| 5 4 | 
             
                include Comparable
         | 
| @@ -16,11 +15,20 @@ module OdeonUk | |
| 16 15 | 
             
                  @slug = name.downcase.gsub(/[^0-9a-z ]/,'').gsub(/\s+/, '-')
         | 
| 17 16 | 
             
                end
         | 
| 18 17 |  | 
| 18 | 
            +
                # All currently listed films showing at a cinema
         | 
| 19 | 
            +
                # @param [Integer] cinema_id id of the cinema on the website
         | 
| 20 | 
            +
                # @return [Array<OdeonUk::Film>]
         | 
| 21 | 
            +
                def self.at(cinema_id)
         | 
| 22 | 
            +
                  showtimes_page(cinema_id).to_a.map do |html|
         | 
| 23 | 
            +
                    new(screenings_parser(html).film_name)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 19 27 | 
             
                # Allows sort on objects
         | 
| 20 28 | 
             
                # @param [OdeonUk::Film] other another film object
         | 
| 21 29 | 
             
                # @return [Integer] -1, 0 or 1
         | 
| 22 | 
            -
                def <=> | 
| 23 | 
            -
                   | 
| 30 | 
            +
                def <=>(other)
         | 
| 31 | 
            +
                  slug <=> other.slug
         | 
| 24 32 | 
             
                end
         | 
| 25 33 |  | 
| 26 34 | 
             
                # Check an object is the same as another object.
         | 
| @@ -28,7 +36,7 @@ module OdeonUk | |
| 28 36 | 
             
                # @return [Boolean] True if both objects are the same exact object, or if
         | 
| 29 37 | 
             
                #   they are of the same type and share an equal slug
         | 
| 30 38 | 
             
                # @note Guided by http://woss.name/2011/01/20/equality-comparison-and-ordering-in-ruby/
         | 
| 31 | 
            -
                def eql? | 
| 39 | 
            +
                def eql?(other)
         | 
| 32 40 | 
             
                  self.class == other.class && self == other
         | 
| 33 41 | 
             
                end
         | 
| 34 42 |  | 
| @@ -41,7 +49,17 @@ module OdeonUk | |
| 41 49 | 
             
                # @return [Integer] hash of slug
         | 
| 42 50 | 
             
                # @note Guided by http://woss.name/2011/01/20/equality-comparison-and-ordering-in-ruby/
         | 
| 43 51 | 
             
                def hash
         | 
| 44 | 
            -
                   | 
| 52 | 
            +
                  slug.hash
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                private
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def self.screenings_parser(html)
         | 
| 58 | 
            +
                  OdeonUk::Internal::FilmWithScreeningsParser.new(html)
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def self.showtimes_page(cinema_id)
         | 
| 62 | 
            +
                  OdeonUk::Internal::ShowtimesPage.new(cinema_id)
         | 
| 45 63 | 
             
                end
         | 
| 46 64 | 
             
              end
         | 
| 47 65 | 
             
            end
         | 
| @@ -1,67 +1,154 @@ | |
| 1 1 | 
             
            module OdeonUk
         | 
| 2 | 
            -
             | 
| 3 2 | 
             
              # Internal utility classes: Do not use
         | 
| 4 3 | 
             
              # @api private
         | 
| 5 4 | 
             
              module Internal
         | 
| 6 | 
            -
             | 
| 7 5 | 
             
                # Parses a chunk of HTML to derive movie showing data
         | 
| 8 6 | 
             
                class FilmWithScreeningsParser
         | 
| 7 | 
            +
                  # CSS selector for a film title inside an individual film HTML
         | 
| 8 | 
            +
                  NAME_CSS      = '.presentation-info h4 a'
         | 
| 9 | 
            +
                  # CSS selector for a group of showtimes inside an individual film HTML
         | 
| 10 | 
            +
                  SHOWTIMES_CSS = '.times-all.accordion-group'
         | 
| 9 11 |  | 
| 10 | 
            -
                  # @param [String]  | 
| 11 | 
            -
                  def initialize( | 
| 12 | 
            -
                    @ | 
| 12 | 
            +
                  # @param [String] html a chunk of html
         | 
| 13 | 
            +
                  def initialize(html)
         | 
| 14 | 
            +
                    @html = html
         | 
| 13 15 | 
             
                  end
         | 
| 14 16 |  | 
| 15 17 | 
             
                  # The film name
         | 
| 16 18 | 
             
                  # @return [String]
         | 
| 17 19 | 
             
                  def film_name
         | 
| 18 | 
            -
                     | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
                     | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
                     | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
                     | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 20 | 
            +
                    @film_name ||= TitleSanitizer.new(raw_film_name).sanitized
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  # array containing hashes of screening attributes
         | 
| 24 | 
            +
                  # @return [Array<Hash>]
         | 
| 25 | 
            +
                  def to_a
         | 
| 26 | 
            +
                    doc.css(SHOWTIMES_CSS).map do |node|
         | 
| 27 | 
            +
                      dimension_parser(node).to_a.map do |hash|
         | 
| 28 | 
            +
                        hash.merge(film_name: film_name)
         | 
| 29 | 
            +
                      end
         | 
| 30 | 
            +
                    end.flatten
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  private
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def doc
         | 
| 36 | 
            +
                    @doc ||= Nokogiri::HTML(@html)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def raw_film_name
         | 
| 40 | 
            +
                    @raw_film_name ||= doc.css(NAME_CSS).children.first.to_s
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def dimension_parser(node)
         | 
| 44 | 
            +
                    DimensionNodeParser.new(node)
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # parses chunk of screenings for a particular screening dimension
         | 
| 49 | 
            +
                class DimensionNodeParser
         | 
| 50 | 
            +
                  # CSS selector for a single screening inside a 'group of showtimes' HTML
         | 
| 51 | 
            +
                  SCREENING_CSS = '.show'
         | 
| 52 | 
            +
                  # CSS selector for showing technology for a 'group of showtimes' HTML
         | 
| 53 | 
            +
                  TECH_CSS = '.tech a'
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  # @param [Nokigiri::Node] node a Nokogiri node object
         | 
| 56 | 
            +
                  def initialize(node)
         | 
| 57 | 
            +
                    @node = node
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  # array containing hashes of screening attributes for dimension
         | 
| 61 | 
            +
                  # @return [Array<Hash>]
         | 
| 62 | 
            +
                  def to_a
         | 
| 63 | 
            +
                    screening_hashes.map do |hash|
         | 
| 64 | 
            +
                      hash.merge(
         | 
| 65 | 
            +
                        dimension: dimension,
         | 
| 66 | 
            +
                        variant: add_imax(hash[:variant])
         | 
| 67 | 
            +
                      )
         | 
| 37 68 | 
             
                    end
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  private
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  def add_imax(original)
         | 
| 74 | 
            +
                    imax? ? "#{original} imax".strip : original
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def dimension
         | 
| 78 | 
            +
                    tech.match(/[23]d/)[0]
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def imax?
         | 
| 82 | 
            +
                    !!tech.match(/imax/)
         | 
| 83 | 
            +
                  end
         | 
| 38 84 |  | 
| 39 | 
            -
             | 
| 40 | 
            -
                     | 
| 41 | 
            -
                    name = name.gsub /\s+\z/, '' # remove trailing spaces
         | 
| 85 | 
            +
                  def screening_hashes
         | 
| 86 | 
            +
                    screening_nodes.map { |node| ScreeningNodeParser.new(node).to_hash }
         | 
| 42 87 | 
             
                  end
         | 
| 43 88 |  | 
| 44 | 
            -
                   | 
| 89 | 
            +
                  def screening_nodes
         | 
| 90 | 
            +
                    @node.css(SCREENING_CSS)
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  def tech
         | 
| 94 | 
            +
                    @tech ||= @node.css(TECH_CSS).text.gsub('in ', '').downcase
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                # parses a single screening
         | 
| 99 | 
            +
                class ScreeningNodeParser
         | 
| 100 | 
            +
                  # regex for time format
         | 
| 101 | 
            +
                  TIME_REGEX = %r(\d+/\d+/\d+ \d{2}\:\d{2})
         | 
| 102 | 
            +
                  # regex for D-Box screenings
         | 
| 103 | 
            +
                  DBOX_REGEX = /D-Box/
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  # @param [Nokigiri::Node] node a Nokogiri node object
         | 
| 106 | 
            +
                  def initialize(node)
         | 
| 107 | 
            +
                    @node = node
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  # hashes of screening attributes
         | 
| 45 111 | 
             
                  # @return [Hash]
         | 
| 46 | 
            -
                   | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
                    @nokogiri_html.css('.times-all.accordion-group').inject({}) do |result, variant_node|
         | 
| 54 | 
            -
                      variant = variant_node.css('.tech a').text.gsub('in ', '').upcase
         | 
| 55 | 
            -
             | 
| 56 | 
            -
                      times_url = variant_node.css('.performance-detail').map do |screening_node|
         | 
| 57 | 
            -
                        [
         | 
| 58 | 
            -
                          tz.local_to_utc(Time.parse(screening_node['title'].match(/\d+\/\d+\/\d+ \d{2}\:\d{2}/).to_s + ' UTC')),
         | 
| 59 | 
            -
                          "http://www.odeon.co.uk#{screening_node['href']}"
         | 
| 60 | 
            -
                        ]
         | 
| 61 | 
            -
                      end
         | 
| 112 | 
            +
                  def to_hash
         | 
| 113 | 
            +
                    {
         | 
| 114 | 
            +
                      booking_url: booking_url,
         | 
| 115 | 
            +
                      time:        utc_time,
         | 
| 116 | 
            +
                      variant:     variant
         | 
| 117 | 
            +
                    }
         | 
| 118 | 
            +
                  end
         | 
| 62 119 |  | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 120 | 
            +
                  private
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                  def booking_url
         | 
| 123 | 
            +
                    link_attr('href').to_s
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  def link_attr(attribute)
         | 
| 127 | 
            +
                    @node.css('a').attribute(attribute)
         | 
| 128 | 
            +
                  end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  def info
         | 
| 131 | 
            +
                    @node.css('i').to_s
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  def time
         | 
| 135 | 
            +
                    Time.parse(time_string)
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  def time_string
         | 
| 139 | 
            +
                    link_attr('title').to_s.match(TIME_REGEX).to_s + ' UTC'
         | 
| 140 | 
            +
                  end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                  def tz
         | 
| 143 | 
            +
                    @tz ||= TZInfo::Timezone.get('Europe/London')
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                  def utc_time
         | 
| 147 | 
            +
                    tz.local_to_utc(time)
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                  def variant
         | 
| 151 | 
            +
                    info.match(DBOX_REGEX) ? 'd-box' : ''
         | 
| 65 152 | 
             
                  end
         | 
| 66 153 | 
             
                end
         | 
| 67 154 | 
             
              end
         | 
| @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            module OdeonUk
         | 
| 2 | 
            +
              # Internal utility classes: Do not use
         | 
| 3 | 
            +
              # @api private
         | 
| 4 | 
            +
              module Internal
         | 
| 5 | 
            +
                # Parses a chunk of HTML to derive showing data for a single films
         | 
| 6 | 
            +
                class ShowtimesPage
         | 
| 7 | 
            +
                  # css selector for film html chunks
         | 
| 8 | 
            +
                  FILM_CSS = '.film-detail'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  # @param [Integer] cinema_id cineworld cinema id
         | 
| 11 | 
            +
                  def initialize(cinema_id)
         | 
| 12 | 
            +
                    @cinema_id = cinema_id
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  # break up the showtimes page into individual chunks for each film
         | 
| 16 | 
            +
                  # @return [Array<String>] html chunks for a film and it's screenings
         | 
| 17 | 
            +
                  def to_a
         | 
| 18 | 
            +
                    film_nodes.map { |node| node.to_s.gsub(/^\s+/, '') }
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  private
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def film_nodes
         | 
| 24 | 
            +
                    @film_nodes ||= showtimes_doc.css(FILM_CSS)
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def showtimes
         | 
| 28 | 
            +
                    @showtimes ||= OdeonUk::Internal::Website.new.showtimes(@cinema_id)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def showtimes_doc
         | 
| 32 | 
            +
                    @showtimes_doc ||= Nokogiri::HTML(showtimes)
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            module OdeonUk
         | 
| 2 | 
            +
              # Internal utility classes: Do not use
         | 
| 3 | 
            +
              # @api private
         | 
| 4 | 
            +
              module Internal
         | 
| 5 | 
            +
                # Sanitize and standardize film titles
         | 
| 6 | 
            +
                class TitleSanitizer
         | 
| 7 | 
            +
                  # strings and regex to be removed
         | 
| 8 | 
            +
                  REMOVE = [
         | 
| 9 | 
            +
                    /\s+[23][dD]/,                 # dimension
         | 
| 10 | 
            +
                    'Autism Friendly Screening -', # autism screening
         | 
| 11 | 
            +
                    /\ACinemagic \d{1,4} \-/,      # cinemagic
         | 
| 12 | 
            +
                    /\(encore.+\)/i,               # encore for NT Live
         | 
| 13 | 
            +
                    /\(live\)/i,                   # live in brackets
         | 
| 14 | 
            +
                    'UKJFF -',                     # UK Jewish festival prefix
         | 
| 15 | 
            +
                  ]
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  # regexes and their replacements
         | 
| 18 | 
            +
                  REPLACE = {
         | 
| 19 | 
            +
                    /Bolshoi - (.*)/               => 'Bolshoi: ',
         | 
| 20 | 
            +
                    /Globe On Screen: (.*)/        => 'Globe: ',
         | 
| 21 | 
            +
                    /Met Opera - (.*)/             => 'Met Opera: ',
         | 
| 22 | 
            +
                    /National Theatre Live - (.*)/ => 'National Theatre: ',
         | 
| 23 | 
            +
                    /NT Live - (.*)/               => 'National Theatre: ',
         | 
| 24 | 
            +
                    /ROH - (.*)/                   => 'Royal Opera House: ',
         | 
| 25 | 
            +
                    /(.*)\- RSC Live \d{1,4}/      => 'Royal Shakespeare Company: '
         | 
| 26 | 
            +
                  }
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  # @param [String] title a film title
         | 
| 29 | 
            +
                  def initialize(title)
         | 
| 30 | 
            +
                    @title = title
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  # sanitized and standardized title
         | 
| 34 | 
            +
                  # @return [String] title
         | 
| 35 | 
            +
                  def sanitized
         | 
| 36 | 
            +
                    @sanitzed ||= begin
         | 
| 37 | 
            +
                      sanitized = @title
         | 
| 38 | 
            +
                      REMOVE.each do |pattern|
         | 
| 39 | 
            +
                        sanitized.gsub! pattern, ''
         | 
| 40 | 
            +
                      end
         | 
| 41 | 
            +
                      REPLACE.each do |pattern, prefix|
         | 
| 42 | 
            +
                        sanitized.gsub!(pattern) { |_| prefix + $1 }
         | 
| 43 | 
            +
                      end
         | 
| 44 | 
            +
                      sanitized.squeeze(' ').strip
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            require 'open-uri'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OdeonUk
         | 
| 4 | 
            +
              # Internal utility classes: Do not use
         | 
| 5 | 
            +
              # @api private
         | 
| 6 | 
            +
              module Internal
         | 
| 7 | 
            +
                # Utility class to make calls to the odeon website
         | 
| 8 | 
            +
                class Website
         | 
| 9 | 
            +
                  # cinema page
         | 
| 10 | 
            +
                  # @param [Integer] cinema_id website id of the cinema
         | 
| 11 | 
            +
                  # @return [String] html of the page
         | 
| 12 | 
            +
                  def cinema(cinema_id)
         | 
| 13 | 
            +
                    get("cinemas/odeon/#{cinema_id}/")
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  # showtimes page for a single cinema
         | 
| 17 | 
            +
                  # @param [Integer] cinema_id website id of the cinema
         | 
| 18 | 
            +
                  # @return [String] html of the page
         | 
| 19 | 
            +
                  def showtimes(cinema_id)
         | 
| 20 | 
            +
                    get("showtimes/week/#{cinema_id}/?siteId=#{cinema_id}")
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  # sitemap page
         | 
| 24 | 
            +
                  # @return [String] html of the page
         | 
| 25 | 
            +
                  def sitemap
         | 
| 26 | 
            +
                    get('sitemap/')
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  private
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def get(path)
         | 
| 32 | 
            +
                    URI("http://www.odeon.co.uk/#{path}").read
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         |