Skip to content

Commit

Permalink
Enhanced Episode Handling, Duration Calculation, and Exception Handli…
Browse files Browse the repository at this point in the history
…ng in Video Models and Services (#501)
  • Loading branch information
brand-it authored Nov 9, 2024
1 parent 3003b55 commit 4912fda
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 22 deletions.
25 changes: 23 additions & 2 deletions app/models/tv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,33 @@ class Tv < Video
end

has_many :seasons, -> { order_by_season_number }, dependent: :destroy, inverse_of: :tv
has_many :episodes, -> { order_by_episode_number }, through: :seasons

before_validation { broadcast(:tv_validating, self) }
before_save { broadcast(:tv_saving, self) }
after_commit { broadcast(:tv_saved, self) }

def min_max_run_time_seconds
(episode_run_time.min * 60)..(episode_run_time.max * 60)
def duration_range
return if ripped_disk_titles_durations.empty? && episodes_runtime.empty?

range = round_up_to_nearest_minute(
duration_stats.interquartile_range ||
episode_runtime_stats.interquartile_range ||
DEFAULT_RANGE
)
average_runtime = duration_stats.weighted_average ||
episode_runtime_stats.weighted_average

return if average_runtime.nil?

(average_runtime - range)..(average_runtime + range)
end

def episode_runtime_stats
@episode_runtime_stats ||= StatsService.call(episodes_runtime)
end

def episodes_runtime
@episodes_runtime ||= episodes.map(&:runtime).uniq.compact_blank
end
end
14 changes: 13 additions & 1 deletion app/models/video.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
#
class Video < ApplicationRecord
include Wisper::Publisher
DEFAULT_RANGE = (60 * 3).seconds # 3 minutes

enum :rating, { 'N/A': 0, NR: 1, 'NC-17': 2, R: 3, 'PG-13': 4, PG: 5, G: 6 }

has_many :disk_titles, dependent: :nullify
Expand All @@ -46,7 +48,11 @@ class Video < ApplicationRecord
validates :title, presence: true

def duration_stats
@duration_stats ||= StatsService.call(ripped_disk_titles&.map(&:duration)&.compact_blank || [])
@duration_stats ||= StatsService.call(ripped_disk_titles_durations)
end

def ripped_disk_titles_durations
@ripped_disk_titles_durations ||= Array.wrap(ripped_disk_titles).map(&:duration).compact_blank
end

def movie?
Expand Down Expand Up @@ -94,4 +100,10 @@ def release_or_air_date
def plex_name
release_or_air_date ? "#{title} (#{release_or_air_date.year})" : title
end

private

def round_up_to_nearest_minute(seconds)
(seconds.to_f / 60).ceil * 60
end
end
12 changes: 1 addition & 11 deletions app/services/episode_disk_title_selector_service.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# frozen_string_literal: true

class EpisodeDiskTitleSelectorService < ApplicationService
DEFAULT_RANGE = (60 * 3).seconds # 3 minutes

Info = Data.define(
:disk_title,
:episode,
Expand Down Expand Up @@ -53,15 +51,7 @@ def ripped?(episode)
end

def within_range?(episode, disk_title)
runtime_range(episode)&.include?(disk_title.duration)
end

def runtime_range(episode)
runtime = episode.tv.duration_stats.weighted_average || episode.runtime
range = episode.tv.duration_stats.interquartile_range || DEFAULT_RANGE
return if runtime.nil?

(runtime - range)...(runtime + range)
episode.tv.duration_range&.include?(disk_title.duration)
end

def selected_disk_titles
Expand Down
24 changes: 22 additions & 2 deletions app/services/stats_service.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# frozen_string_literal: true

class StatsService < ApplicationService
Z_SCORE_THRESHOLD = 5
Info = Data.define(
:median,
:weighted_average,
:median_difference,
:weighted_average_difference,
:interquartile_range
:interquartile_range,
:z_score,
:differences
)

param :list, Types::Array.of(Types::Coercible::Integer)
Expand All @@ -17,7 +20,9 @@ def call
weighted_average,
median_difference,
weighted_average_difference,
interquartile_range
interquartile_range,
z_score,
differences
)
end

Expand Down Expand Up @@ -65,6 +70,21 @@ def interquartile_range
filtered_differences.max
end

def z_score
return if differences.empty?

mean = differences.sum.to_f / differences.size

# Calculate standard deviation
variance = differences.map { |x| (x - mean)**2 }.sum / differences.size
stdev = Math.sqrt(variance)

differences.select do |x|
z_score = (x - mean) / stdev
z_score.abs < Z_SCORE_THRESHOLD
end.max
end

def calc_weight_average(array, median)
weights = array.map do |item|
1.0 / (1.0 + (item - median).abs)
Expand Down
25 changes: 22 additions & 3 deletions app/views/seasons/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,28 @@
Title Select
<small>
<% if @tv.duration_stats.weighted_average %>
(<%= distance_of_time_in_words(@tv.duration_stats.weighted_average - @tv.duration_stats.interquartile_range, 0, include_seconds: false) %>
-
<%= distance_of_time_in_words(@tv.duration_stats.weighted_average + @tv.duration_stats.interquartile_range, 0, include_seconds: false) %>)
(<%= distance_of_time_in_words(@tv.duration_range.min, 0, include_seconds: params[:debug] == 'true') %>
..
<%= distance_of_time_in_words(@tv.duration_range.max, 0, include_seconds: params[:debug] == 'true') %>)

<% if params[:debug] == 'true' %>
<br>
median: <%= distance_of_time_in_words @tv.duration_stats.median %>
<br>
weighted average: <%= distance_of_time_in_words @tv.duration_stats.weighted_average %>
<br>
weighted average difference: <%= distance_of_time_in_words @tv.duration_stats.weighted_average_difference %>
<br>
interquartile range: <%= distance_of_time_in_words(@tv.duration_stats.interquartile_range) %>
<br>
median difference: <%= distance_of_time_in_words(@tv.duration_stats.median_difference) %>
<br>
z score: <%= distance_of_time_in_words @tv.duration_stats.z_score %>
<br>
Durations: <%= @tv.ripped_disk_titles_durations.sort.uniq.join(', ') %>
<br>
Differences: <%= @tv.duration_stats.differences.join(', ') %>
<% end %>
<% end %>
</small>
</th>
Expand Down
2 changes: 1 addition & 1 deletion current_version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v5.12.0
v5.13.0
10 changes: 10 additions & 0 deletions spec/services/episode_disk_title_selector_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
let(:disk) { build_stubbed(:disk, disk_titles: [disk_title]) }
let(:disk_title) { build_stubbed(:disk_title, duration: 10.minutes) }

before do
tv.association(:ripped_disk_titles).target = [disk_title]
tv.association(:ripped_disk_titles).loaded!
end

it { expect(call.first).to be_a(described_class::Info) }
it { expect(call.first.disk_title.id).to eq(disk_title.id) }
it { expect(call.first.episode.id).to eq(episodes.first.id) }
Expand All @@ -26,6 +31,11 @@
let(:disk_title_a) { build_stubbed(:disk_title, duration: 10.minutes) }
let(:disk_title_b) { build_stubbed(:disk_title, duration: 10.minutes) }

before do
tv.association(:ripped_disk_titles).target = [disk_title_a, disk_title_b]
tv.association(:ripped_disk_titles).loaded!
end

it { expect(call.first.episode.id).to eq(episodes.first.id) }
it { expect(call.first.disk_title.id).to eq(disk_title_a.id) }
it { expect(call.second.episode.id).to eq(episodes.second.id) }
Expand Down
9 changes: 7 additions & 2 deletions spec/services/stats_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@

context 'when a bunch of values are given' do
let(:list) { [2579, 2498, 2549, 2575, 2573, 2486, 2518, 2580, 2494, 2577, 2575, 2575, 2577, 2443, 2578, 2441, 2521, 2561, 2580, 2534, 2571, 2565, 5062] }
let(:differences) do
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 26, 27, 28, 29, 30, 31, 32, 35, 36, 37, 39, 40, 41, 43, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 67, 71, 73,
75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 89, 91, 92, 93, 94, 106, 108, 118, 120, 122, 124, 128, 130, 132, 134, 135, 136, 137, 138, 139, 2482, 2483, 2484, 2485, 2487, 2489, 2491, 2497, 2501, 2513, 2528, 2541, 2544,
2564, 2568, 2576, 2619, 2621]
end

it { is_expected.to eq(described_class::Info.new(2575, 2573.7497394471216, 62.5, 62.79612125669019, 139)) }
it { is_expected.to eq(described_class::Info.new(2575, 2573.7497394471216, 62.5, 62.79612125669019, 139, 2621, differences)) }
end

context 'when nothing is given' do
let(:list) { [] }

it { is_expected.to eq(described_class::Info.new(nil, nil, nil, nil, nil)) }
it { is_expected.to eq(described_class::Info.new(nil, nil, nil, nil, nil, nil, [])) }
end
end
end

0 comments on commit 4912fda

Please sign in to comment.