Fullscreen/yt

View on GitHub
lib/yt/associations/has_reports.rb

Summary

Maintainability
C
1 day
Test Coverage
module Yt
  module Associations
    # @private
    # Provides methods to access the analytics reports of a resource.
    module HasReports
      # @!macro [new] reports
      #   Returns the reports for the given metrics grouped by the given dimension.
      #   @!method reports(options = {})
      #   @param [Hash] options the metrics, time-range and dimensions for the reports.
      #   @option options [Array<Symbol>] :only The metrics to generate reports
      #     for.
      #   @option options [Symbol] :by (:day) The dimension to collect metrics
      #     by. Accepted values are: +:day+, +:month+.
      #   @option options [#to_date] :since The first day of the time-range.
      #     Also aliased as +:from+.
      #   @option options [#to_date] :until The last day of the time-range.
      #     Also aliased as +:to+.
      #   @return [Hash<Symbol, Hash>] the reports for each metric specified.
      #   @example Get the views and estimated minutes watched by day for last week:
      #     resource.reports only: [:views, :estimated_minutes_watched] since: 1.week.ago, by: :day
      #     # => {views: {Wed, 8 May 2014 => 12, Thu, 9 May 2014 => 34, …}, estimated_minutes_watched: {Wed, 8 May 2014 => 9, Thu, 9 May 2014 => 6, …}}

      # @!macro [new] report
      #   Returns the $1 grouped by the given dimension.
      #   @!method $1(options = {})
      #   @param [Hash] options the time-range and dimensions for the $1.
      #   @option options [#to_date] :since The first day of the time-range.
      #     Also aliased as +:from+.
      #   @option options [#to_date] :until The last day of the time-range.
      #     Also aliased as +:to+.

      # @!macro [new] report_with_day
      #   @return [Hash<Date, $2>] if grouped by day, the $1
      #     for each day in the time-range.
      #   @example Get the $1 for each day of last week:
      #     resource.$1 since: 2.weeks.ago, until: 1.week.ago, by: :day
      #     # => {Wed, 8 May 2014 => 12.0, Thu, 9 May 2014 => 34.0, …}
      #   @return [Hash<Range<Date, Date>, $2>] if grouped by month, the $1
      #     for each month in the time-range.
      #   @example Get the $1 for this and last month:
      #     resource.$1 since: 1.month.ago, by: :month
      #     # => {Wed, 01 Apr 2014..Thu, 30 Apr 2014 => 12.0, Fri, 01 May 2014..Sun, 31 May 2014 => 34.0, …}
      #   @macro report

      # @!macro [new] report_with_range
      #   @return [Hash<Symbol, $2>] if grouped by range, the $1
      #     for the entire time-range (under the key +:total+).
      #   @example Get the $1 for the whole last week:
      #     resource.$1 since: 2.weeks.ago, until: 1.week.ago, by: :range
      #     # => {total: 564.0}

      # @!macro [new] report_with_country
      #   @option options [<String, Hash>] :in The country to limit the $1
      #     to. Can either be the two-letter ISO-3166-1 code of a country, such
      #     as +"US"+, or a Hash with the code in the +:country+ key, such
      #     as +{country: "US"}+.
      #   @example Get the $1 for the whole last week in France only:
      #     resource.$1 since: 2.weeks.ago, until: 1.week.ago, by: :range, in: 'FR'
      #     # => {total: 44.0}

      # @!macro [new] report_with_country_and_state
      #   @option options [<String, Hash>] :in The location to limit the $1
      #     to. Can either be the two-letter ISO-3166-1 code of a country, such
      #     as +"US"+, or a Hash that either contains the +:country+ key, such
      #     as +{country: "US"}+ or the +:state+ key, such as +{state: "TX"}+.
      #     Note that YouTube API only provides data for US states.
      #   @example Get the $1 for the whole last week in Texas only:
      #     resource.$1 since: 2.weeks.ago, until: 1.week.ago, by: :range, in: {state: 'TX'}
      #     # => {total: 19.0}

      # @!macro [new] report_by_day
      #   @option options [Symbol] :by (:day) The dimension to collect $1 by.
      #     Accepted values are: +:day+, +:month+.
      #   @macro report_with_day

      # @!macro [new] report_by_day_and_country
      #   @option options [Symbol] :by (:day) The dimension to collect $1 by.
      #     Accepted values are: +:day+, +:month+, :+range+.
      #   @macro report_with_day
      #   @macro report_with_range
      #   @macro report_with_country

      # @!macro [new] report_by_day_and_state
      #   @option options [Symbol] :by (:day) The dimension to collect $1 by.
      #     Accepted values are: +:day+, +:month+, :+range+.
      #   @macro report_with_day
      #   @macro report_with_range
      #   @macro report_with_country_and_state

      # @!macro [new] report_with_video_dimensions
      #   @return [Hash<Symbol, $2>] if grouped by playback location, the
      #     $1 for each traffic playback location.
      #   @example Get yesterday’s $1 grouped by playback location:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :playback_location
      #     # => {embedded: 53.0, watch: 467.0, …}
      #   @return [Hash<Yt::Video, $2>] if grouped by related video, the
      #     $1 for each related video.
      #   @example Get yesterday’s $1 by related video, eager-loading the snippet of each video::
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :related_video, includes: [:snippet]
      #     # => {#<Yt::Video @id=…> => 33.0, #<Yt::Video @id=…> => 12.0, …}
      #   @return [Hash<Symbol, $2>] if grouped by device type, the
      #     $1 for each device type.
      #   @example Get yesterday’s $1 by search term:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :search_term
      #     # => {"fullscreen" => 33.0, "good music" => 12.0, …}
      #   @return [Hash<String, $2>] if grouped by search term, the
      #     $1 for each search term that led viewers to the content.
      #   @example Get yesterday’s $1 by URL that referred to the resource:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :referrer
      #     # => {"Google Search" => 33.0, "ytimg.com" => 12.0, …}
      #   @return [Hash<String, $2>] if grouped by search term, the
      #     $1 for each search term that led viewers to the content.
      #   @example Get yesterday’s $1 by device type:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :device_type
      #     # => {mobile: 133.0, tv: 412.0, …}
      #   @return [Hash<Symbol, $2>] if grouped by traffic source, the
      #     $1 for each traffic source.
      #   @example Get yesterday’s $1 by traffic source:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :traffic_source
      #     # => {advertising: 543.0, playlist: 92.0, …}
      #   @macro report_with_day
      #   @macro report_with_range

      # @!macro [new] report_by_video_dimensions
      #   @option options [Symbol] :by (:day) The dimension to collect $1 by.
      #     Accepted values are: +:day+, +:month+, +:range+,
      #     +:traffic_source+,+:search_term+, +:playback_location+,
      #     +:related_video+, +:embedded_player_location+.
      #   @option options [Array<Symbol>] :includes ([:id]) if grouped by
      #     related video, the parts of each video to eager-load. Accepted
      #     values are: +:id+, +:snippet+, +:status+, +:statistics+,
      #     +:content_details+.
      #   @return [Hash<Symbol, $2>] if grouped by embedded player location,
      #     the $1 for each embedded player location.
      #   @example Get yesterday’s $1 by embedded player location:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :embedded_player_location
      #     # => {"fullscreen.net" => 92.0, "yahoo.com" => 14.0, …}
      #   @macro report_with_video_dimensions
      #   @macro report_with_country_and_state

      # @!macro [new] report_with_channel_dimensions
      #   @option options [Array<Symbol>] :includes ([:id]) if grouped by
      #     video, related video, or playlist, the parts of each video or
      #     playlist to eager-load. Accepted values are: +:id+, +:snippet+,
      #     +:status+, +:statistics+, +:content_details+.
      #   @return [Hash<Yt::Video, $2>] if grouped by video, the
      #     $1 for each video.
      #   @example Get yesterday’s $1 by video, eager-loading the status and statistics of each video:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :video, includes: [:status, :statistics]
      #     # => {#<Yt::Video @id=…> => 33.0, #<Yt::Video @id=…> => 12.0, …}
      #   @return [Hash<Yt::Playlist, $2>] if grouped by playlist, the
      #     $1 for each playlist.
      #   @example Get yesterday’s $1 by playlist:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :playlist
      #     # => {#<Yt::Playlist @id=…> => 33.0, #<Yt::Playlist @id=…> => 12.0, …}
      #   @macro report_with_video_dimensions

      # @!macro [new] report_by_channel_dimensions
      #   @option options [Symbol] :by (:day) The dimension to collect $1 by.
      #     Accepted values are: +:day+, +:month+, +:range+,
      #     +:traffic_source+, +:search_term+, +:playback_location+, +:video+,
      #     +:related_video+, +:playlist+, +:embedded_player_location+.
      #   @return [Hash<Symbol, $2>] if grouped by embedded player location,
      #     the $1 for each embedded player location.
      #   @example Get yesterday’s $1 by embedded player location:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :embedded_player_location
      #     # => {"fullscreen.net" => 92.0, "yahoo.com" => 14.0, …}
      #   @macro report_with_channel_dimensions
      #   @macro report_with_country_and_state

      # @!macro [new] report_by_playlist_dimensions
      #   @option options [Symbol] :by (:day) The dimension to collect $1 by.
      #     Accepted values are: +:day+, +:month+, +:range+,
      #     +:traffic_source+, +:playback_location+, +:related_video+, +:video+,
      #     +:playlist+.
      #   @macro report_with_channel_dimensions
      #   @macro report_with_country_and_state

      # @!macro [new] report_by_gender_and_age_group
      #   @option options [Symbol] :by (:gender_age_group) The dimension to
      #     show viewer percentage by.
      #     Accepted values are: +:gender+, +:age_group+, +:gender_age_group+.
      #   @return [Hash<Symbol, $2>] if grouped by gender, the
      #     viewer percentage by gender.
      #   @example Get yesterday’s viewer percentage by gender:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :gender
      #     # => {female: 53.0, male: 47.0}
      #   @return [Hash<String, $2>] if grouped by age group, the
      #     viewer percentage by age group.
      #   @example Get yesterday’s $1 grouped by age group:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, by: :age_group
      #     # => {"18-24" => 4.54, "35-24" => 12.31, "45-34" => 8.92, …}
      #   @return [Hash<Symbol, [Hash<String, $2>]>] if grouped by gender
      #     and age group, the viewer percentage by gender/age group.
      #   @example Get yesterday’s $1 by gender and age group:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago
      #     # => {female: {"18-24" => 12.12, "25-34" => 16.16, …}, male:…}
      #   @example Get yesterday’s $1 by gender and age group in France only:
      #     resource.$1 since: 1.day.ago, until: 1.day.ago, in: 'FR'
      #     # => {female: {"18-24" => 16.12, "25-34" => 13.16, …}, male:…}
      #   @macro report
      #   @macro report_with_country_and_state

      # Defines a public instance methods to access the reports of a
      # resource for a specific metric.
      # @param [Symbol] metric the metric to access the reports of.
      # @param [Class] type The class to cast the returned values to.
      # @example Adds +comments+ on a Channel resource.
      #   class Channel < Resource
      #     has_report :comments, Integer
      #   end
      def has_report(metric, type)
        require 'yt/collections/reports'

        define_metric_method metric
        define_reports_method metric, type
        define_range_metric_method metric
        define_all_metric_method metric, type
      end

    private

      def define_reports_method(metric, type)
        (@metrics ||= {})[metric] = type
        define_method :reports do |options = {}|
          from = options[:since] || options[:from] || (options[:by].in?([:day, :month]) ? 5.days.ago : '2005-02-01')
          to = options[:until] || options[:to] || Date.today
          location = options[:in]
          country = location.is_a?(Hash) ? location[:country] : location
          state = location[:state] if location.is_a?(Hash)
          dimension = options[:by] || (metric == :viewer_percentage ? :gender_age_group : :range)
          videos = options[:videos]
          historical = options[:historical].to_s if [true, false].include?(options[:historical])
          if dimension == :month
            from = from.to_date.beginning_of_month
            to = to.to_date.beginning_of_month
          end
          date_range = Range.new *[from, to].map(&:to_date)

          only = options.fetch :only, []
          reports = Collections::Reports.of(self).tap do |reports|
            reports.metrics = self.class.instance_variable_get(:@metrics).select{|k, v| k.in? only}
          end
          reports.within date_range, country, state, dimension, videos, historical
        end unless defined?(reports)
      end

      def define_metric_method(metric)
        define_method metric do |options = {}|
          from = options[:since] || options[:from] || (options[:by].in?([:day, :month]) ? 5.days.ago : '2005-02-01')
          to = options[:until] || options[:to] || Date.today
          location = options[:in]
          country = location.is_a?(Hash) ? location[:country] : location
          state = location[:state] if location.is_a?(Hash)
          dimension = options[:by] || (metric == :viewer_percentage ? :gender_age_group : :range)
          videos = options[:videos]
          historical = options[:historical].to_s if [true, false].include?(options[:historical])
          if dimension == :month
            from = from.to_date.beginning_of_month
            to = to.to_date.beginning_of_month
          end
          range = Range.new *[from, to].map(&:to_date)

          ivar = instance_variable_get "@#{metric}_#{dimension}_#{country}_#{state}"
          instance_variable_set "@#{metric}_#{dimension}_#{country}_#{state}", ivar || {}
          results = case dimension
          when :day
            Hash[*range.flat_map do |date|
              [date, instance_variable_get("@#{metric}_#{dimension}_#{country}_#{state}")[date] ||= send("range_#{metric}", range, dimension, country, state, videos, historical)[date]]
            end]
          else
            instance_variable_get("@#{metric}_#{dimension}_#{country}_#{state}")[range] ||= send("range_#{metric}", range, dimension, country, state, videos, historical)
          end
          lookup_class = case options[:by]
            when :video, :related_video then Yt::Collections::Videos
            when :playlist then Yt::Collections::Playlists
          end
          if lookup_class
            includes = options.fetch(:includes, [:id]).join(',').camelize(:lower)
            items = lookup_class.new(auth: auth).where(part: includes, id: results.keys.join(','))
            results.transform_keys{|id| items.find{|item| item.id == id}}.reject{|k, _| k.nil?}
          else
            results
          end
        end
      end

      def define_range_metric_method(metric)
        define_method "range_#{metric}" do |date_range, dimension, country, state, videos, historical|
          ivar = instance_variable_get "@range_#{metric}_#{dimension}_#{country}_#{state}"
          instance_variable_set "@range_#{metric}_#{dimension}_#{country}_#{state}", ivar || {}
          instance_variable_get("@range_#{metric}_#{dimension}_#{country}_#{state}")[date_range] ||= send("all_#{metric}").within date_range, country, state, dimension, videos, historical
        end
        private "range_#{metric}"
      end

      def define_all_metric_method(metric, type)
        define_method "all_#{metric}" do
          # @note Asking for the "estimated_revenue" metric of a day in which a channel
          # made 0 USD returns the wrong "nil". But adding to the request the
          # "estimatedMinutesWatched" metric returns the correct value 0.
          metrics = {metric => type}
          metrics[:estimated_minutes_watched] = Integer if metric == :estimated_revenue
          Collections::Reports.of(self).tap{|reports| reports.metrics = metrics}
        end
        private "all_#{metric}"
      end
    end
  end
end