diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3da0a2df..852c5f35 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 1000` -# on 2024-07-14 00:55:29 UTC using RuboCop version 1.65.0. +# on 2024-07-15 20:10:17 UTC using RuboCop version 1.65.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -27,21 +27,25 @@ Lint/UnreachableCode: Exclude: - 'lib/tasks/upload.rake' -# Offense count: 8 +# Offense count: 11 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. Metrics/AbcSize: Exclude: - 'app/listeners/mkv_progress_listener.rb' - 'app/models/video_blob.rb' + - 'app/services/create_disks_service.rb' + - 'app/services/create_mkv_service.rb' - 'app/services/eject_disk_service.rb' - 'app/workers/rip_worker.rb' - 'app/workers/scan_plex_worker.rb' -# Offense count: 1 +# Offense count: 3 # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ClassLength: Exclude: - 'app/listeners/mkv_progress_listener.rb' + - 'app/models/video_blob.rb' + - 'app/workers/scan_plex_worker.rb' # Offense count: 3 # Configuration parameters: AllowedMethods, AllowedPatterns, Max. @@ -50,11 +54,13 @@ Metrics/CyclomaticComplexity: - 'app/controllers/start_controller.rb' - 'app/models/video_blob.rb' -# Offense count: 5 +# Offense count: 9 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Exclude: - 'app/models/video_blob.rb' + - 'app/services/create_disks_service.rb' + - 'app/services/create_mkv_service.rb' - 'app/workers/scan_plex_worker.rb' # Offense count: 3 @@ -84,7 +90,7 @@ RSpec/MultipleMemoizedHelpers: Exclude: - 'spec/workers/rip_worker_spec.rb' -# Offense count: 11 +# Offense count: 12 Rails/I18nLocaleTexts: Exclude: - 'app/controllers/config/make_mkvs_controller.rb' diff --git a/app/components/movie_title_table_component.html.erb b/app/components/movie_title_table_component.html.erb index 5e77727d..42d4bce2 100644 --- a/app/components/movie_title_table_component.html.erb +++ b/app/components/movie_title_table_component.html.erb @@ -1,22 +1,46 @@ <% if disks.any? %> - - - - - - - - - - - - <% disks.each do |disk| %> + <%= simple_form_for :movies, url: rip_movie_path(movie) do |f| %> + <% disk = disks.first %> + <%= hidden_field_tag :disk_id, disk&.id %> +
#Name(video title)Duration (<%= distance_of_time_in_words @movie.movie_runtime.seconds %>)Size
+ + + + + + + + + + + + <% disk.disk_titles.sort_by { |d| movie.runtime_range.include?(d.duration) ? 0 : 1 }.each do |disk_title| %> + <%= hidden_field_tag "movies[][disk_title_id]", disk_title.id %> <% text_class = 'text-primary-emphasis' if movie.runtime_range.include?(disk_title.duration) %> - <% text_class = 'text-success-emphasis' if movie.disk_titles.include?(disk_title) %> + <% text_class = 'text-success-emphasis' if movie.video_blobs.any? %> - + + + + - <% end %> - <% end %> - -
#RippedUploadedName(video title)Duration (<%= distance_of_time_in_words @movie.movie_runtime.seconds %>)Size
<%= disk_title.id %><%= disk_title.id %> +

+ <% if movie.ripped_disk_titles.any? %> + <%= icon('square-check') %> + <% else %> + <%= icon('square') %> + <% end %> +

+
+

+ <% if disk_title.video_blob&.uploaded? %> + <%= icon('square-check') %> + <% else %> + <%= icon('square') %> + <% end %> +

+
<%= disk_title.name %> <% if disk_title.video %> @@ -24,20 +48,24 @@ <% end %> <%= distance_of_time_in_words(disk_title.duration.seconds) %> + <%= number_to_human_size(disk_title.size, precision: 3) %> <% if disk_title.size >= free_disk_space %> - WARNING: Not enough space available to rip need another <%= number_to_human_size(disk_title.size - free_disk_space, precision: 3) %> + WARNING: There Might Not enough space available to rip, needs another <%= number_to_human_size(disk_title.size - free_disk_space, precision: 3) %> <% end %> - <%= link_to 'Rip', rip_movie_path(movie, disk_title_id: disk_title.id), data: { turbo_method: :post }, class: 'btn btn-outline-light' %> + + <%= select_tag "movies[][extra_type]", options_for_select(VideoBlob.extra_types.keys), prompt: "Extra Type" %>
+ +
+ <%= f.button :submit, 'Rip Disk' %> +
+ + <% end %> <% elsif LoadDiskWorker.job.active? %>

Stand by the disk is still being loading from the CD drive this could take a while. Page will update once the disk is ready so no need to refresh the page but if you do it won't hurt anything.

<% else %> diff --git a/app/components/movie_title_table_component.rb b/app/components/movie_title_table_component.rb index 7cb901ff..59e7aeb1 100644 --- a/app/components/movie_title_table_component.rb +++ b/app/components/movie_title_table_component.rb @@ -2,6 +2,7 @@ class MovieTitleTableComponent < ViewComponent::Base extend Dry::Initializer + include IconHelper option :disks, Types::Coercible::Array.of(Types.Instance(Disk)) option :movie, Types.Instance(Movie) diff --git a/app/controllers/config/make_mkvs_controller.rb b/app/controllers/config/make_mkvs_controller.rb index 1c8bd2dd..9ab7b6f2 100644 --- a/app/controllers/config/make_mkvs_controller.rb +++ b/app/controllers/config/make_mkvs_controller.rb @@ -14,15 +14,21 @@ def create @config_make_mkv = Config::MakeMkv.new(make_mkv_params) if @config_make_mkv.save - redirect_to root_path, notice: 'Make MKV Config was successfully created.' + redirect_to root_path, success: 'Make MKV Config was successfully created.' else + flash.now[:error] = 'Could not create MKV Config' render :new end end def update - flash[:success] = 'Updated Make MKV' if @config_make_mkv.update(make_mkv_params) - redirect_to root_path + if @config_make_mkv.update(make_mkv_params) + flash[:success] = 'Updated Make MKV' + redirect_to root_path + else + flash.now[:error] = 'Could not update MKV Config' + render :edit + end end def install @@ -42,7 +48,7 @@ def set_make_mkv end def make_mkv_params - params.require(:config_slack).permit(:settings_makemkvcon_path, :settings_registration_key) + params.require(:config_make_mkv).permit(:settings_makemkvcon_path, :settings_registration_key) end end end diff --git a/app/controllers/config/plexes_controller.rb b/app/controllers/config/plexes_controller.rb index 04933e65..39fad224 100644 --- a/app/controllers/config/plexes_controller.rb +++ b/app/controllers/config/plexes_controller.rb @@ -22,6 +22,7 @@ def create @config_plex = Config::Plex.new(config_plex_params) if @config_plex.save + ScanPlexWorker.perform_async redirect_to root_path, notice: 'Plex was successfully created.' else render :new @@ -30,6 +31,7 @@ def create def update if @config_plex.update(config_plex_params) + ScanPlexWorker.perform_async redirect_to root_path, notice: 'Plex was successfully updated.' else render :edit diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb index 313aab79..fb92c04d 100644 --- a/app/controllers/movies_controller.rb +++ b/app/controllers/movies_controller.rb @@ -16,9 +16,29 @@ def show # rip_movie POST /movies/:id/rip(.:format) def rip movie = Movie.find(params[:id]) - disk_title = DiskTitle.find(params[:disk_title_id]) - disk_title.update!(video: movie) - job = RipWorker.perform_async(disk_id: disk_title.disk.id, disk_title_ids: [disk_title.id]) + disk = Disk.find(params[:disk_id]) + disk_titles = rip_disk_titles(disk, movie) + job = RipWorker.perform_async( + disk_id: disk.id, + disk_title_ids: disk_titles.map(&:id), + extra_types: movies_params.pluck(:extra_type).compact_blank + ) redirect_to job_path(job) end + + private + + def rip_disk_titles(disk, movie) + movies_params.filter_map do |movie_params| + next if movie_params[:extra_type].blank? + + disk_title = disk.disk_titles.find(movie_params[:disk_title_id]) + disk_title.update!(video: movie) + disk_title + end + end + + def movies_params + params.required(:movies) + end end diff --git a/app/listeners/the_movie_db/video_listener.rb b/app/listeners/the_movie_db/video_listener.rb index db54aa1d..878eea6d 100644 --- a/app/listeners/the_movie_db/video_listener.rb +++ b/app/listeners/the_movie_db/video_listener.rb @@ -2,11 +2,11 @@ module TheMovieDb class VideoListener - def tv_saving(tv) + def tv_validating(tv) TheMovieDb::TvUpdateService.call(tv) end - def movie_saving(movie) + def movie_validating(movie) TheMovieDb::MovieUpdateService.call(movie) end end diff --git a/app/listeners/upload_progress_listener.rb b/app/listeners/upload_progress_listener.rb index 072661bc..c08ba4c9 100644 --- a/app/listeners/upload_progress_listener.rb +++ b/app/listeners/upload_progress_listener.rb @@ -7,7 +7,7 @@ class UploadProgressListener delegate :render, to: :ApplicationController - option :disk_title, Types.Instance(::DiskTitle) + option :video_blob, Types.Instance(::VideoBlob) option :file_size, Types::Integer attr_reader :completed @@ -63,13 +63,13 @@ def next_update end def title - if disk_title.video.is_a?(Tv) - episode = disk_title.episode + if video_blob.video.is_a?(Tv) + episode = video_blob.episode season = episode.season - "#{disk_title.video.title} - S#{season.season_number}E#{episode.episode_number} " \ + "#{video_blob.video.title} - S#{season.season_number}E#{episode.episode_number} " \ "#{episode.name}" else - disk_title.video.title + video_blob.video.title end end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 71fbba5b..e2edc4dc 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -2,4 +2,10 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true + + def unmark_for_destruction + return unless instance_variable_defined?(:@marked_for_destruction) + + remove_instance_variable(:@marked_for_destruction) + end end diff --git a/app/models/config/slack.rb b/app/models/config/slack.rb index 6fd43c81..52583ab9 100644 --- a/app/models/config/slack.rb +++ b/app/models/config/slack.rb @@ -1,5 +1,15 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: configs +# +# id :integer not null, primary key +# settings :text +# type :string default("Config"), not null +# created_at :datetime not null +# updated_at :datetime not null +# class Config class Slack < Config setting do |s| diff --git a/app/models/disk_title.rb b/app/models/disk_title.rb index b9d0edbb..48e052fa 100644 --- a/app/models/disk_title.rb +++ b/app/models/disk_title.rb @@ -15,6 +15,7 @@ # episode_id :integer # mkv_progress_id :bigint # title_id :integer not null +# video_blob_id :integer # video_id :integer # # Indexes @@ -23,6 +24,7 @@ # index_disk_titles_on_episode_id (episode_id) # index_disk_titles_on_mkv_progress_id (mkv_progress_id) # index_disk_titles_on_video (video_id) +# index_disk_titles_on_video_blob_id (video_blob_id) # class DiskTitle < ApplicationRecord include ActionView::Helpers::DateHelper @@ -31,6 +33,8 @@ class DiskTitle < ApplicationRecord belongs_to :episode, optional: true belongs_to :disk, optional: true + belongs_to :video_blob, optional: true + scope :not_ripped, -> { where(ripped_at: nil) } scope :ripped, -> { where.not(ripped_at: nil) } @@ -41,37 +45,4 @@ def duration def to_label "##{title_id} #{name} #{distance_of_time_in_words(duration)}" end - - def tmp_plex_path - require_movie_or_episode! - video.is_a?(Tv) ? episode.tmp_plex_path : video.tmp_plex_path - end - - def plex_path - require_movie_or_episode! - video.is_a?(Tv) ? episode.plex_path : video.plex_path - end - - def plex_name - require_movie_or_episode! - video.is_a?(Tv) ? episode.plex_name : video.plex_name - end - - def tmp_plex_dir - require_movie_or_episode! - video.is_a?(Tv) ? episode.tmp_plex_dir : video.tmp_plex_dir - end - - def tmp_plex_path_exists? - return false if video.nil? - - video.is_a?(Tv) ? episode.tmp_plex_path_exists? : video.tmp_plex_path_exists? - end - - def require_movie_or_episode! - return if video.is_a?(Movie) - return if video.is_a?(Tv) && episode - - raise 'requires episode or movie to rip' - end end diff --git a/app/models/episode.rb b/app/models/episode.rb index bf298be2..c92aa73b 100644 --- a/app/models/episode.rb +++ b/app/models/episode.rb @@ -31,6 +31,7 @@ class Episode < ApplicationRecord scope :order_by_episode_number, -> { order(:episode_number) } delegate :tv, to: :season + delegate :title, :plex_name, to: :tv, prefix: true def runtime @runtime ||= super&.minutes @@ -42,63 +43,11 @@ def runtime_range @runtime_range ||= (runtime - 3.minutes)...(runtime + 3.minutes) end - def plex_path - raise 'plex config is missing and is required' unless Config::Plex.any? - - @plex_path ||= Pathname.new( - "#{Config::Plex.newest.settings_tv_path}/#{tv_plex_name}/#{season_name}/#{plex_name}" - ) - end - - def tmp_plex_dir - @tmp_plex_dir ||= Rails.root.join("tmp/tv/#{tv_plex_name}/#{season_name}") - end - - def tmp_plex_path - @tmp_plex_path ||= tmp_plex_dir.join(plex_name) - end - def plex_name - "#{episode_plex_name} - s#{format_season_number}e#{format_episode_number} - #{name}.mkv" - end - - def episode_first_air_date - season.tv.episode_first_air_date - end - - def title - season.tv.title - end - - def season_name - "Season #{format_season_number}" - end - - def format_season_number - format('%02d', season.season_number) + [tv.plex_name, "s#{season.format_season_number}e#{format_episode_number}", name].compact_blank.join(' - ') end def format_episode_number format('%02d', episode_number) end - - def episode_plex_name - if episode_first_air_date - "#{title} (#{episode_first_air_date.year})" - else - title - end - end - - def tv_plex_name - @tv_plex_name ||= if episode_first_air_date - "#{title} (#{episode_first_air_date.year})" - else - title - end - end - - def tmp_plex_path_exists? - File.exist?(tmp_plex_path) - end end diff --git a/app/models/movie.rb b/app/models/movie.rb index e2ad0eec..9708d192 100644 --- a/app/models/movie.rb +++ b/app/models/movie.rb @@ -15,7 +15,6 @@ # poster_path :string # rating :integer default("N/A"), not null # release_date :date -# synced_on :datetime # title :string # type :string # created_at :datetime not null @@ -27,6 +26,7 @@ # index_videos_on_type_and_the_movie_db_id (type,the_movie_db_id) UNIQUE # class Movie < Video + before_validation { broadcast(:movie_validating, self) } before_save { broadcast(:movie_saving, self) } after_commit { broadcast(:movie_saved, self) } @@ -40,32 +40,4 @@ class Movie < Video def runtime_range (movie_runtime - MOVIE_RUNNTIME_MARGIN)..(movie_runtime + MOVIE_RUNNTIME_MARGIN) end - - def plex_path - raise 'plex config is missing and is required' unless Config::Plex.any? - - @plex_path ||= Pathname.new("#{Config::Plex.newest.settings_movie_path}/#{plex_name}/#{plex_name}.mkv") - end - - def tmp_plex_dir - @tmp_plex_dir ||= Rails.root.join("tmp/movies/#{plex_name}") - end - - def tmp_plex_path - @tmp_plex_path ||= tmp_plex_dir.join("#{plex_name}.mkv") - end - - def plex_name - @plex_name ||= (release_date ? "#{title} (#{release_date.year})" : title) - end - - def update_maxlength(max) - return config.maxlength = (max + MOVIE_DURATION_MARGIN) if max.to_i > (config.minlength / 60) - - config.maxlength = nil - end - - def tmp_plex_path_exists? - File.exist?(tmp_plex_path) - end end diff --git a/app/models/season.rb b/app/models/season.rb index 377647d5..06688aae 100644 --- a/app/models/season.rb +++ b/app/models/season.rb @@ -33,4 +33,12 @@ class Season < ApplicationRecord before_save { broadcast(:season_saving, self) } after_commit { broadcast(:season_saved, id, async: true) } + + def season_name + "Season #{format_season_number}" + end + + def format_season_number + format('%02d', season_number) + end end diff --git a/app/models/tv.rb b/app/models/tv.rb index afa46e39..8abcaa84 100644 --- a/app/models/tv.rb +++ b/app/models/tv.rb @@ -15,7 +15,6 @@ # poster_path :string # rating :integer default("N/A"), not null # release_date :date -# synced_on :datetime # title :string # type :string # created_at :datetime not null @@ -39,6 +38,7 @@ class Tv < Video has_many :seasons, -> { order_by_season_number }, dependent: :destroy, inverse_of: :tv + before_validation { broadcast(:tv_validating, self) } before_save { broadcast(:tv_saving, self) } after_commit { broadcast(:tv_saved, self) } diff --git a/app/models/video.rb b/app/models/video.rb index 1a4acfac..a558696c 100644 --- a/app/models/video.rb +++ b/app/models/video.rb @@ -15,7 +15,6 @@ # poster_path :string # rating :integer default("N/A"), not null # release_date :date -# synced_on :datetime # title :string # type :string # created_at :datetime not null @@ -28,11 +27,11 @@ # class Video < ApplicationRecord include Wisper::Publisher - 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 - has_many :video_blobs, dependent: :destroy + has_many :ripped_disk_titles, -> { ripped }, class_name: 'DiskTitle', dependent: false, inverse_of: :video + has_many :video_blobs, dependent: :nullify has_many :optimized_video_blobs, lambda { VideoBlob.optimized }, class_name: 'VideoBlob', inverse_of: :video, dependent: :destroy @@ -41,6 +40,16 @@ class Video < ApplicationRecord scope :optimized, -> { includes(:optimized_video_blobs).where.not(video_blobs: { id: nil }) } scope :optimized_with_checksum, -> { optimized.merge(VideoBlob.checksum) } + validates :title, presence: true + + def movie? + is_a?(::Movie) + end + + def tv? + is_a?(::Tv) + end + def credits @credits ||= "TheMovieDb::#{type}::Credits".constantize.new(the_movie_db_id).results end @@ -60,6 +69,14 @@ def ratings end def release_or_air_date - release_date || episode_first_air_date + if is_a?(Movie) + release_date + elsif is_a?(Tv) + episode_first_air_date + end + end + + def plex_name + release_or_air_date ? "#{title} (#{release_or_air_date.year})" : title end end diff --git a/app/models/video_blob.rb b/app/models/video_blob.rb index 5d76e1e9..31217e70 100644 --- a/app/models/video_blob.rb +++ b/app/models/video_blob.rb @@ -4,24 +4,28 @@ # # Table name: video_blobs # -# id :integer not null, primary key -# byte_size :bigint not null -# checksum :text -# content_type :string not null -# filename :string not null -# key :string not null -# metadata :text -# optimized :boolean default(FALSE), not null -# service_name :string not null -# created_at :datetime not null -# updated_at :datetime not null -# episode_id :bigint -# video_id :integer +# id :integer not null, primary key +# byte_size :bigint not null +# checksum :text +# content_type :string not null +# extra_type :integer default("feature_films") +# extra_type_number :integer not null +# filename :string not null +# key :string not null +# metadata :text +# optimized :boolean default(FALSE), not null +# uploaded_on :datetime +# created_at :datetime not null +# updated_at :datetime not null +# episode_id :bigint +# video_id :integer # # Indexes # -# index_video_blobs_on_key_and_service_name (key,service_name) UNIQUE -# index_video_blobs_on_video (video_id) +# idx_on_extra_type_number_video_id_extra_type_1978193db6 (extra_type_number,video_id,extra_type) UNIQUE +# index_video_blobs_on_key (key) UNIQUE +# index_video_blobs_on_key_and_service_name (key) UNIQUE +# index_video_blobs_on_video (video_id) # class VideoBlob < ApplicationRecord Movie = Struct.new(:title, :year) do @@ -34,6 +38,9 @@ def episode end end TvShow = Struct.new(:title, :year, :season, :episode) + EXTRA_TYPES = %i[feature_films behind_the_scenes deleted_scenes featurettes interviews scenes shorts trailers + other].freeze + EXTRA_TYPE_TO_DIRECTORY = EXTRA_TYPES.to_h { [_1, _1.to_s.humanize.titleize] } VIDEO_FORMATS = [ '.avi', '.mp4', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.mpeg', @@ -53,37 +60,136 @@ def episode TV_SHOW_WITHOUT_NUMBER = /#{MATCHER_WITH_YEAR}.*-\s+(?.*)\s+-\s+(?.*).*#{Regexp.union(VIDEO_FORMATS)}/ TV_SHOW_WITHOUT_YEAR = /#{TITLE_MATCHER}.*-\s+#{TV_SHOW_SEASON_EPISODE}\s+-\s+(?.*)\s+-\s+(?.*).*#{Regexp.union(VIDEO_FORMATS)}/ TV_SHOW_NUMBER_ONLY = /#{TV_SHOW_SEASON_EPISODE}\.*#{Regexp.union(VIDEO_FORMATS)}/ + + enum :extra_type, EXTRA_TYPES + belongs_to :video, optional: true belongs_to :episode, optional: true + has_many :disk_titles, dependent: :nullify scope :optimized, -> { where(optimized: true) } scope :checksum, -> { where.not(checksum: nil) } scope :missing_checksum, -> { where(checksum: nil) } - delegate :title, :year, :episode, :season, to: :parsed, allow_nil: true + delegate :title, :year, :episode, :season, to: :parsed, allow_nil: true, prefix: true + delegate :plex_name, to: :video, prefix: true, allow_nil: true + delegate :plex_name, to: :episode, prefix: true, allow_nil: true - def tv_show? - return false if Config::Plex.newest&.tv_path.blank? + validates :key, presence: true, uniqueness: true - key&.starts_with?(Config::Plex.newest.tv_path) || false + before_validation :set_extra_type_number + + def self.build_from_disk_title(disk_title, extra_type) + extra_type = extra_type.presence || EXTRA_TYPES.first + blob = VideoBlob.new( + video: disk_title.video, + episode: disk_title.episode, + extra_type: + ) + VideoBlob.find_or_initialize_by( + video: disk_title.video, + episode: disk_title.episode, + filename: blob.plex_name.to_s, + key: blob.plex_path.to_s, + content_type: 'video/x-matroska', + extra_type: + ) + end + + def uploaded? + uploaded_on.present? + end + + def tv_show? + video&.tv? || key_tv_show? end def movie? - return false if Config::Plex.newest&.movie_path.blank? + video&.movie? || key_movie? + end - key&.starts_with?(Config::Plex.newest.movie_path) || false + def plex_path + if feature_films? + Pathname.new("#{plex_dir_name}/#{plex_name}.mkv") + else + Pathname.new("#{plex_dir_name}/#{extra_type_directory} ##{extra_type_number}.mkv") + end + end + + def tmp_plex_path + if feature_films? + tmp_plex_dir.join("#{plex_name}.mkv") + else + tmp_plex_dir.join("#{extra_type_directory} ##{extra_type_number}.mkv") + end + end + + def tmp_plex_dir + if feature_films? + Rails.root.join("tmp/#{video.type.parameterize}/#{subdirectories}") + else + Rails.root.join("tmp/#{video.type.parameterize}/#{video_plex_name}/#{extra_type_directory}") + end + end + + def tmp_plex_path_exists? + File.exist?(tmp_plex_path) + end + + def plex_name + if video&.movie? + video_plex_name + elsif video&.tv? + episode_plex_name + end + end + + def extra_type_number + super || (VideoBlob.where(video:, extra_type:).pluck(:extra_type_number).max.to_i + 1) end private + def plex_dir_name + if feature_films? + Pathname.new("#{plex_root_path}/#{subdirectories}") + else + Pathname.new("#{plex_root_path}/#{video_plex_name}/#{extra_type_directory}") + end + end + + def subdirectories + if video&.movie? + video_plex_name + elsif video&.tv? + "#{video_plex_name}/#{episode.season.season_name}" + end + end + + def extra_type_directory + EXTRA_TYPE_TO_DIRECTORY[extra_type.to_sym] + end + def parsed - if movie? + if key_movie? parsed_movie - elsif tv_show? + elsif key_tv_show? parsed_tv_show end end + def key_movie? + return false if Config::Plex.newest&.movie_path.blank? + + key&.starts_with?(Config::Plex.newest.movie_path) || false + end + + def key_tv_show? + return false if Config::Plex.newest&.tv_path.blank? + + key&.starts_with?(Config::Plex.newest.tv_path) || false + end + def parsed_tv_show return @parsed_tv_show if @parsed_tv_filename @@ -122,7 +228,28 @@ def parsed_movie def directory_name return '' if key.blank? - path = movie? ? Config::Plex.newest.movie_path : Config::Plex.newest.tv_path - @directory_name ||= key.gsub("#{path}/", '').split('/').first.to_s + @directory_name ||= key.gsub("#{plex_root_path}/", '').split('/').first.to_s + end + + def convert_to_extra_type + return '' if key.blank? + + @convert_to_extra_type ||= key.gsub("#{video_path}/", '').split('/').first.to_s + end + + def plex_root_path + raise 'plex config is missing and is required' if Config::Plex.newest.nil? + + key_movie? || video&.movie? ? Config::Plex.newest.movie_path : Config::Plex.newest.tv_path + end + + def video_path + "#{plex_root_path}/#{directory_name}" + end + + def set_extra_type_number + return if attributes[:extra_type_number] + + self.extra_type_number = VideoBlob.where(video:, extra_type:).pluck(:extra_type_number).max.to_i + 1 end end diff --git a/app/services/create_disks_service.rb b/app/services/create_disks_service.rb index c9debc2e..4d2c0cab 100644 --- a/app/services/create_disks_service.rb +++ b/app/services/create_disks_service.rb @@ -17,7 +17,11 @@ def call .tap do |disk| disk.update!(loading: true) broadcast_loading!(disk.name) - disk.disk_info.each { update_disk_title(disk, _1) } + disk.disk_titles.each(&:mark_for_distruction) + disk.disk_info.each do |info| + disk_title = find_or_build_disk_title(disk, info) + disk_title.unmark_for_destruction + end disk.update!(loading: false) end end @@ -34,16 +38,18 @@ def broadcast_loading!(name = nil) cable_ready.broadcast end - def update_disk_title(disk, title) - disk_title = find_or_build_disk_title(disk, title) - disk_title.assign_attributes name: title.filename, - size: title.size_in_bytes, - duration: title.duration_seconds - end - def find_or_build_disk_title(disk, title) - disk.not_ripped_disk_titles.find { |t| t.title_id == title.id } || - disk.not_ripped_disk_titles.build(title_id: title.id) + disk.disk_titles.find do |disk_title| + disk_title.title_id == title.id.to_i && + title.filename == disk_title.name && + title.size_in_bytes.to_i == disk_title.size && + title.duration_seconds.to_i == disk_title.duration + end || disk.disk_titles.build( + title_id: title.id, + name: title.filename, + size: title.size_in_bytes, + duration: title.duration_seconds + ) end def drives diff --git a/app/services/create_mkv_service.rb b/app/services/create_mkv_service.rb index 0e2b580d..2180da61 100644 --- a/app/services/create_mkv_service.rb +++ b/app/services/create_mkv_service.rb @@ -10,6 +10,7 @@ class Error < StandardError; end TMP_DIR = Rails.root.join('tmp/videos') option :disk_title, Types.Instance(DiskTitle) + option :extra_type, Types::Coercible::String, default: -> { VideoBlob::EXTRA_TYPES.first } def self.call(...) new(...).call @@ -17,10 +18,11 @@ def self.call(...) def call broadcast(:start) - Result.new(tmp_path, create_mkv.success?).tap do |result| + Result.new(video_blob.tmp_plex_path, create_mkv.success?).tap do |result| if result.success? + video_blob.update!(byte_size:) rename_file - disk_title.update!(ripped_at: Time.current) + disk_title.update!(ripped_at: Time.current, video_blob:) broadcast(:success) else broadcast(:failure) @@ -30,7 +32,7 @@ def call private - def create_mkv # rubocop:disable Metrics/MethodLength + def create_mkv @create_mkv ||= Open3.popen2e(cmd) do |stdin, std_out_err, wait_thr| stdin.close while raw_line = std_out_err.gets # rubocop:disable Lint/AssignmentInCondition @@ -46,7 +48,11 @@ def create_mkv # rubocop:disable Metrics/MethodLength end def rename_file - File.rename(tmp_dir.join(disk_title.name), tmp_path) + File.rename(tmp_dir.join(disk_title.name), video_blob.tmp_plex_path) + end + + def byte_size + File.size(tmp_dir.join(disk_title.name)) end def cmd @@ -63,11 +69,11 @@ def cmd end def tmp_dir - @tmp_dir ||= tmp_path.dirname.tap(&method(:recreate_dir)) + @tmp_dir ||= video_blob.tmp_plex_dir.tap(&method(:recreate_dir)) end - def tmp_path - @tmp_path ||= disk_title.tmp_plex_path + def video_blob + @video_blob ||= VideoBlob.build_from_disk_title(disk_title, extra_type) end def recreate_dir(dir) diff --git a/app/services/ftp/upload_mkv_service.rb b/app/services/ftp/upload_mkv_service.rb index 540adfd0..189da2be 100644 --- a/app/services/ftp/upload_mkv_service.rb +++ b/app/services/ftp/upload_mkv_service.rb @@ -5,7 +5,7 @@ class UploadMkvService < Base extend Dry::Initializer include Wisper::Publisher - option :disk_title, Types.Instance(DiskTitle) + option :video_blob, Types.Instance(::VideoBlob) def call broadcast(:started) @@ -19,14 +19,14 @@ def call private def ftp_destroy_if_file_exists - ftp.delete(disk_title.plex_path) + ftp.delete(video_blob.plex_path) rescue Net::FTPPermError => e Rails.logger.debug { "Net::FTPPermError #{__method__} #{e.message}" } end def ftp_create_directory current_dir = '' - disk_title.plex_path.dirname.to_s.split('/').each do |directory| + video_blob.plex_path.dirname.to_s.split('/').each do |directory| next current_dir += '' if directory.blank? current_dir += "/#{directory}" @@ -37,17 +37,21 @@ def ftp_create_directory end def ftp_upload_file - ftp.putbinaryfile(file, disk_title.plex_path) do |chunk| + ftp.putbinaryfile(file, video_blob.plex_path) do |chunk| broadcast(:update_progress, chunk_size: chunk.size) end end + def mark_as_uploaded! + video_blob.update!(uploaded_on: Time.current) + end + def tmp_destroy_folder - FileUtils.rm_rf(disk_title.tmp_plex_path) + FileUtils.rm_rf(video_blob.tmp_plex_path) end def file - @file ||= File.new(disk_title.tmp_plex_path) + @file ||= File.new(video_blob.tmp_plex_path) end end end diff --git a/app/services/ftp/video_scanner_service.rb b/app/services/ftp/video_scanner_service.rb index 974ae9b0..3812c858 100644 --- a/app/services/ftp/video_scanner_service.rb +++ b/app/services/ftp/video_scanner_service.rb @@ -34,15 +34,13 @@ def build_video(path, entry) end def find_or_initialize_by(key) - service_name = safe_encode(plex_config.settings_ftp_host) - preloaded_video_blobs.dig(safe_encode(key), service_name) || VideoBlob.new(key: safe_encode(key), service_name:) + preloaded_video_blobs[safe_encode(key)] || VideoBlob.new(key: safe_encode(key)) end def preloaded_video_blobs @preloaded_video_blobs ||= {}.tap do |hash| - VideoBlob.find_each do |video_blob| - hash[video_blob.key] ||= {} - hash[video_blob.key][video_blob.service_name] = video_blob + VideoBlob.includes(:video).find_each do |video_blob| + hash[video_blob.key] ||= video_blob end end end diff --git a/app/services/the_movie_db/movie_update_service.rb b/app/services/the_movie_db/movie_update_service.rb index 432a1908..e1d31ca5 100644 --- a/app/services/the_movie_db/movie_update_service.rb +++ b/app/services/the_movie_db/movie_update_service.rb @@ -30,7 +30,6 @@ def call def movie_params the_movie_db_details.slice(*PERMITTED_PARAMS).tap do |params| params[:movie_runtime] = convert_min_to_seconds(the_movie_db_details['runtime']) - params[:synced_on] = Time.current end end diff --git a/app/services/the_movie_db/tv_update_service.rb b/app/services/the_movie_db/tv_update_service.rb index 633479c9..6fece487 100644 --- a/app/services/the_movie_db/tv_update_service.rb +++ b/app/services/the_movie_db/tv_update_service.rb @@ -36,7 +36,6 @@ def tv_params the_movie_db_details.slice(*PERMITTED_PARAMS).tap do |params| params[:episode_distribution_runtime] = Array.wrap(the_movie_db_details['episode_run_time']).sort params[:episode_first_air_date] = the_movie_db_details['first_air_date'] - params[:synced_on] = Time.current end end diff --git a/app/views/layouts/application.erb b/app/views/layouts/application.erb index cab8fe40..4ba4369e 100644 --- a/app/views/layouts/application.erb +++ b/app/views/layouts/application.erb @@ -9,7 +9,7 @@
<%= render 'layouts/header' %> -
+
<%= render 'layouts/bg_progress' %> diff --git a/app/views/seasons/show.html.erb b/app/views/seasons/show.html.erb index 86f02089..b5fcca74 100644 --- a/app/views/seasons/show.html.erb +++ b/app/views/seasons/show.html.erb @@ -48,10 +48,10 @@

- <% if episode.video_blobs.any? %> - <%= icon('square-check') %> - <% else %> + <% if episode.video_blob&.uploaded? %> <%= icon('square') %> + <% else %> + <%= icon('square-check') %> <% end %>

diff --git a/app/workers/continue_upload_worker.rb b/app/workers/continue_upload_worker.rb index 8aeb4b66..3c8d24be 100644 --- a/app/workers/continue_upload_worker.rb +++ b/app/workers/continue_upload_worker.rb @@ -2,16 +2,16 @@ class ContinueUploadWorker < ApplicationWorker def enqueue? - pending_disk_titles.any? + pending_video_blobs.any? end def perform - pending_disk_titles.each do |disk_title| - UploadWorker.perform_async(disk_title_id: disk_title.id) + pending_video_blobs.find_each do |video_blob| + UploadWorker.perform_async(video_blob_id: video_blob.id) end end - def pending_disk_titles - @pending_disk_titles ||= DiskTitle.all.select(&:tmp_plex_path_exists?) + def pending_video_blobs + @pending_video_blobs ||= ::VideoBlob.where(uploaded_on: nil) end end diff --git a/app/workers/rip_worker.rb b/app/workers/rip_worker.rb index 2109d5d8..da74c0e2 100644 --- a/app/workers/rip_worker.rb +++ b/app/workers/rip_worker.rb @@ -8,6 +8,7 @@ class RipWorker < ApplicationWorker option :disk_id, Types::Integer option :disk_title_ids, Types::Array.of(Types::Integer) + option :extra_types, Types::Array.of(Types::String), optional: true, default: -> { [] } def perform if create_mkvs.all?(&:success?) @@ -24,8 +25,9 @@ def perform private def create_mkvs - disk_titles.filter_map do |disk_title| - service = CreateMkvService.new(disk_title:) + disk_title_ids.zip(extra_types).filter_map do |disk_title_id, extra_type| + disk_title = DiskTitle.find(disk_title_id) + service = CreateMkvService.new(disk_title:, extra_type:) service.subscribe(MkvProgressListener.new(job:, disk_title:)) result = service.call job.save! @@ -54,7 +56,7 @@ def redirect_url end def upload_mkv(disk_title) - UploadWorker.perform_async(disk_title_id: disk_title.id) + UploadWorker.perform_async(video_blob_id: disk_title.video_blob_id) end def disk diff --git a/app/workers/scan_plex_worker.rb b/app/workers/scan_plex_worker.rb index 9503734b..85a07f78 100644 --- a/app/workers/scan_plex_worker.rb +++ b/app/workers/scan_plex_worker.rb @@ -6,14 +6,16 @@ def perform plex_videos.map do |blob| blob.video = find_or_create_video(blob) blob.episode = search_for_episode(blob, blob.video) + blob.uploaded_on ||= Time.current blob.save! self.completed += 1 percent_completed = (completed / plex_videos.size.to_f * 100) + next if next_update.future? + broadcast_progress( in_progress_component('Updating Database...', percent_completed) ) end - @next_update = Time.current broadcast_progress(completed_component) end @@ -24,8 +26,6 @@ def completed private def broadcast_progress(component) - return if next_update.future? - cable_ready[BroadcastChannel.channel_name].morph \ selector: "##{component.dom_id}", html: render(component, layout: false) @@ -63,14 +63,14 @@ def in_progress_component(message, completed, show_percentage: true) end def search_for_movie(blob) - options = { query: blob.title, year: blob.year }.compact + options = { query: blob.parsed_title, year: blob.parsed_year }.compact search = TheMovieDb::Search::Movie.new(**options) if options[:query].present? search&.results&.dig('results', 0, 'id') end def search_for_tv_show(blob) - options = { query: blob.title, year: blob.year }.compact + options = { query: blob.parsed_title, year: blob.parsed_year }.compact search = TheMovieDb::Search::Tv.new(**options) if options[:query].present? search&.results&.dig('results', 0, 'id') @@ -79,12 +79,12 @@ def search_for_tv_show(blob) def search_for_episode(blob, video) return unless video.is_a?(::Tv) - season = video.seasons.find { _1.season_number == blob.season } + season = video.seasons.find { _1.season_number == blob.parsed_season } return if season.nil? season.subscribe(TheMovieDb::EpisodesListener.new) season.save! - season.episodes.find { _1.episode_number == blob.episode } + season.episodes.find { _1.episode_number == blob.parsed_episode } end def find_or_create_video(blob) @@ -95,17 +95,24 @@ def find_or_create_video(blob) end return if the_movie_db_id.nil? + find_or_initialize_video(blob, the_movie_db_id).tap do |m| + m.subscribe(TheMovieDb::VideoListener.new) + m.save! + end + end + + def find_or_initialize_video(blob, the_movie_db_id) model = if blob.tv_show? Tv elsif blob.movie? Movie - else - return end - model.find_or_initialize_by(the_movie_db_id:).tap do |m| - m.subscribe(TheMovieDb::VideoListener.new) - m.save! - end + videos.find { _1.the_movie_db_id == the_movie_db_id } || + model&.new(the_movie_db_id:)&.tap { videos.push(_1) } + end + + def videos + @videos ||= Video.all.to_a end def plex_videos diff --git a/app/workers/upload_worker.rb b/app/workers/upload_worker.rb index 361a5afe..60ffec5f 100644 --- a/app/workers/upload_worker.rb +++ b/app/workers/upload_worker.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true class UploadWorker < ApplicationWorker - option :disk_title_id, Types::Integer + option :video_blob_id, Types::Integer def perform - disk_title = DiskTitle.find(disk_title_id) - service = Ftp::UploadMkvService.new(disk_title:) - service.subscribe(UploadProgressListener.new(disk_title:, file_size: disk_title.size)) + video_blob = VideoBlob.find(video_blob_id) + service = Ftp::UploadMkvService.new(video_blob:) + service.subscribe(UploadProgressListener.new(video_blob:, file_size: video_blob.byte_size)) service.call end end diff --git a/config/database.yml b/config/database.yml index dccf173b..6573fe5a 100644 --- a/config/database.yml +++ b/config/database.yml @@ -6,8 +6,8 @@ # default: &default adapter: sqlite3 - pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i + 2 %> - timeout: 15000 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i + 4 %> + timeout: 5000 development: <<: *default diff --git a/current_version.txt b/current_version.txt index b299be97..857572fc 100644 --- a/current_version.txt +++ b/current_version.txt @@ -1 +1 @@ -v3.3.0 +v4.0.0 diff --git a/db/migrate/20240714152912_add_extras_to_disk_titles.rb b/db/migrate/20240714152912_add_extras_to_disk_titles.rb new file mode 100644 index 00000000..1c5a7d2a --- /dev/null +++ b/db/migrate/20240714152912_add_extras_to_disk_titles.rb @@ -0,0 +1,5 @@ +class AddExtrasToDiskTitles < ActiveRecord::Migration[7.1] + def change + add_column :video_blobs, :extra_type, :integer, default: 0 + end +end diff --git a/db/migrate/20240714182717_add_reference_to_disk_title_from_video_blob.rb b/db/migrate/20240714182717_add_reference_to_disk_title_from_video_blob.rb new file mode 100644 index 00000000..3e50d81a --- /dev/null +++ b/db/migrate/20240714182717_add_reference_to_disk_title_from_video_blob.rb @@ -0,0 +1,5 @@ +class AddReferenceToDiskTitleFromVideoBlob < ActiveRecord::Migration[7.1] + def change + add_reference :disk_titles, :video_blob + end +end diff --git a/db/migrate/20240714183648_drop_service_name_from_video_blobs.rb b/db/migrate/20240714183648_drop_service_name_from_video_blobs.rb new file mode 100644 index 00000000..0ee9e87f --- /dev/null +++ b/db/migrate/20240714183648_drop_service_name_from_video_blobs.rb @@ -0,0 +1,9 @@ +class DropServiceNameFromVideoBlobs < ActiveRecord::Migration[7.1] + def up + remove_column :video_blobs, :service_name + end + + def down + add_column :video_blobs, :service_name, :string + end +end diff --git a/db/migrate/20240714200900_add_extra_type_number_to_video_blob.rb b/db/migrate/20240714200900_add_extra_type_number_to_video_blob.rb new file mode 100644 index 00000000..89f08bf9 --- /dev/null +++ b/db/migrate/20240714200900_add_extra_type_number_to_video_blob.rb @@ -0,0 +1,6 @@ +class AddExtraTypeNumberToVideoBlob < ActiveRecord::Migration[7.1] + def change + VideoBlob.destroy_all + add_column :video_blobs, :extra_type_number, :integer + end +end diff --git a/db/migrate/20240714203719_backfill_extra_type_number.rb b/db/migrate/20240714203719_backfill_extra_type_number.rb new file mode 100644 index 00000000..7de759cf --- /dev/null +++ b/db/migrate/20240714203719_backfill_extra_type_number.rb @@ -0,0 +1,9 @@ +class BackfillExtraTypeNumber < ActiveRecord::Migration[7.1] + def change + VideoBlob.all.each do |blob| + blob.extra_type_number = VideoBlob.where(episode:, video:, extra_type:) + .pluck(:extra_type_number).max.to_i + 1 + blob.save! + end + end +end diff --git a/db/migrate/20240714203948_add_null_constraint_to_extra_type_number.rb b/db/migrate/20240714203948_add_null_constraint_to_extra_type_number.rb new file mode 100644 index 00000000..bf9f7988 --- /dev/null +++ b/db/migrate/20240714203948_add_null_constraint_to_extra_type_number.rb @@ -0,0 +1,5 @@ +class AddNullConstraintToExtraTypeNumber < ActiveRecord::Migration[7.1] + def change + change_column_null :video_blobs, :extra_type_number, false + end +end diff --git a/db/migrate/20240714204214_add_uniq_key_constraint.rb b/db/migrate/20240714204214_add_uniq_key_constraint.rb new file mode 100644 index 00000000..5dc4fbe3 --- /dev/null +++ b/db/migrate/20240714204214_add_uniq_key_constraint.rb @@ -0,0 +1,5 @@ +class AddUniqKeyConstraint < ActiveRecord::Migration[7.1] + def change + add_index :video_blobs, :key, if_not_exists: true, unique: true + end +end diff --git a/db/migrate/20240714204402_add_uniq_key_constraint_extra_type_number.rb b/db/migrate/20240714204402_add_uniq_key_constraint_extra_type_number.rb new file mode 100644 index 00000000..17908886 --- /dev/null +++ b/db/migrate/20240714204402_add_uniq_key_constraint_extra_type_number.rb @@ -0,0 +1,5 @@ +class AddUniqKeyConstraintExtraTypeNumber < ActiveRecord::Migration[7.1] + def change + add_index :video_blobs, [:extra_type_number, :video_id, :extra_type], unique: true + end +end diff --git a/db/migrate/20240714235006_remove_column_synced_on.rb b/db/migrate/20240714235006_remove_column_synced_on.rb new file mode 100644 index 00000000..42cf755f --- /dev/null +++ b/db/migrate/20240714235006_remove_column_synced_on.rb @@ -0,0 +1,5 @@ +class RemoveColumnSyncedOn < ActiveRecord::Migration[7.1] + def change + remove_column :videos, :synced_on, :datetime + end +end diff --git a/db/migrate/20240715021135_add_uploaded_to_video_blob.rb b/db/migrate/20240715021135_add_uploaded_to_video_blob.rb new file mode 100644 index 00000000..9c94415c --- /dev/null +++ b/db/migrate/20240715021135_add_uploaded_to_video_blob.rb @@ -0,0 +1,5 @@ +class AddUploadedToVideoBlob < ActiveRecord::Migration[7.1] + def change + add_column :video_blobs, :uploaded_on, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index a696af52..a24aecb9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_07_11_043012) do +ActiveRecord::Schema[7.1].define(version: 2024_07_15_021135) do create_table "configs", force: :cascade do |t| t.string "type", default: "Config", null: false t.text "settings" @@ -30,9 +30,11 @@ t.integer "video_id" t.integer "episode_id" t.datetime "ripped_at" + t.integer "video_blob_id" t.index ["disk_id"], name: "index_disk_titles_on_disk_id" t.index ["episode_id"], name: "index_disk_titles_on_episode_id" t.index ["mkv_progress_id"], name: "index_disk_titles_on_mkv_progress_id" + t.index ["video_blob_id"], name: "index_disk_titles_on_video_blob_id" t.index ["video_id"], name: "index_disk_titles_on_video" end @@ -121,7 +123,6 @@ t.string "filename", null: false t.string "content_type", null: false t.text "metadata" - t.string "service_name", null: false t.bigint "byte_size", null: false t.boolean "optimized", default: false, null: false t.text "checksum" @@ -129,7 +130,12 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "episode_id" - t.index ["key", "service_name"], name: "index_video_blobs_on_key_and_service_name", unique: true + t.integer "extra_type", default: 0 + t.integer "extra_type_number", null: false + t.datetime "uploaded_on" + t.index ["extra_type_number", "video_id", "extra_type"], name: "idx_on_extra_type_number_video_id_extra_type_1978193db6", unique: true + t.index ["key"], name: "index_video_blobs_on_key", unique: true + t.index ["key"], name: "index_video_blobs_on_key_and_service_name", unique: true t.index ["video_id"], name: "index_video_blobs_on_video" end @@ -146,7 +152,6 @@ t.float "popularity" t.date "release_date" t.date "episode_first_air_date" - t.datetime "synced_on" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "rating", default: 0, null: false diff --git a/spec/factories/disk_titles.rb b/spec/factories/disk_titles.rb index 5a42b567..87f52172 100644 --- a/spec/factories/disk_titles.rb +++ b/spec/factories/disk_titles.rb @@ -15,6 +15,7 @@ # episode_id :integer # mkv_progress_id :bigint # title_id :integer not null +# video_blob_id :integer # video_id :integer # # Indexes @@ -23,6 +24,7 @@ # index_disk_titles_on_episode_id (episode_id) # index_disk_titles_on_mkv_progress_id (mkv_progress_id) # index_disk_titles_on_video (video_id) +# index_disk_titles_on_video_blob_id (video_blob_id) # FactoryBot.define do factory :disk_title do diff --git a/spec/factories/movies.rb b/spec/factories/movies.rb index 27ab40c8..aafaaa73 100644 --- a/spec/factories/movies.rb +++ b/spec/factories/movies.rb @@ -15,7 +15,6 @@ # poster_path :string # rating :integer default("N/A"), not null # release_date :date -# synced_on :datetime # title :string # type :string # created_at :datetime not null diff --git a/spec/factories/tvs.rb b/spec/factories/tvs.rb index 2686463b..8a861e67 100644 --- a/spec/factories/tvs.rb +++ b/spec/factories/tvs.rb @@ -15,7 +15,6 @@ # poster_path :string # rating :integer default("N/A"), not null # release_date :date -# synced_on :datetime # title :string # type :string # created_at :datetime not null diff --git a/spec/factories/video_blobs.rb b/spec/factories/video_blobs.rb index 3a9d9c46..78cd85d4 100644 --- a/spec/factories/video_blobs.rb +++ b/spec/factories/video_blobs.rb @@ -4,31 +4,38 @@ # # Table name: video_blobs # -# id :integer not null, primary key -# byte_size :bigint not null -# checksum :text -# content_type :string not null -# filename :string not null -# key :string not null -# metadata :text -# optimized :boolean default(FALSE), not null -# service_name :string not null -# created_at :datetime not null -# updated_at :datetime not null -# episode_id :bigint -# video_id :integer +# id :integer not null, primary key +# byte_size :bigint not null +# checksum :text +# content_type :string not null +# extra_type :integer default("feature_films") +# extra_type_number :integer not null +# filename :string not null +# key :string not null +# metadata :text +# optimized :boolean default(FALSE), not null +# uploaded_on :datetime +# created_at :datetime not null +# updated_at :datetime not null +# episode_id :bigint +# video_id :integer # # Indexes # -# index_video_blobs_on_key_and_service_name (key,service_name) UNIQUE -# index_video_blobs_on_video (video_id) +# idx_on_extra_type_number_video_id_extra_type_1978193db6 (extra_type_number,video_id,extra_type) UNIQUE +# index_video_blobs_on_key (key) UNIQUE +# index_video_blobs_on_key_and_service_name (key) UNIQUE +# index_video_blobs_on_video (video_id) # FactoryBot.define do factory :video_blob do filename { 'Back to the Future Part III.mkv' } - key { 'error-incidunt/omnis_eos/perferendis-iure/Back to the Future Part III.mkv' } - service_name { :ftp } + sequence(:key) { "error-incidunt/#{_1}/perferendis-iure/Back to the Future Part III.mkv" } content_type { 'video/x-matroska' } byte_size { 123_456_789 } + + after(:stub) do |blob, _context| + blob.send(:set_extra_type_number) + end end end diff --git a/spec/factories/videos.rb b/spec/factories/videos.rb index 9db13628..02f1d24a 100644 --- a/spec/factories/videos.rb +++ b/spec/factories/videos.rb @@ -15,7 +15,6 @@ # poster_path :string # rating :integer default("N/A"), not null # release_date :date -# synced_on :datetime # title :string # type :string # created_at :datetime not null diff --git a/spec/listeners/the_movie_db/video_listener_spec.rb b/spec/listeners/the_movie_db/video_listener_spec.rb index 314f07c8..e0faa98a 100644 --- a/spec/listeners/the_movie_db/video_listener_spec.rb +++ b/spec/listeners/the_movie_db/video_listener_spec.rb @@ -8,7 +8,7 @@ before { create(:config_the_movie_db) } describe '#tv_saving', :vcr, freeze: Time.zone.local(1990) do - subject(:tv_saving) { described_class.new.tv_saving(tv) } + subject(:tv_saving) { described_class.new.tv_validating(tv) } let(:expected_attributes) do { @@ -31,7 +31,6 @@ 'from Earth who explore the galaxy and defend against alien threats such ' \ "as the Goa'uld, Replicators, and the Ori.", 'type' => 'Tv', - 'synced_on' => Time.zone.local(1990), 'updated_at' => nil, 'created_at' => nil, 'release_date' => nil diff --git a/spec/listeners/upload_progress_listener_spec.rb b/spec/listeners/upload_progress_listener_spec.rb index 9b314367..fb820b76 100644 --- a/spec/listeners/upload_progress_listener_spec.rb +++ b/spec/listeners/upload_progress_listener_spec.rb @@ -8,10 +8,10 @@ describe '#update_progress' do subject(:update_progress) { listener.update_progress(chunk_size: 10) } - let(:disk_title) { build_stubbed(:disk_title) } + let(:video_blob) { build_stubbed(:video_blob) } let(:args) do { - disk_title:, + video_blob:, file_size: 12 } end diff --git a/spec/models/disk_title_spec.rb b/spec/models/disk_title_spec.rb index 60a41c26..841d59d5 100644 --- a/spec/models/disk_title_spec.rb +++ b/spec/models/disk_title_spec.rb @@ -15,6 +15,7 @@ # episode_id :integer # mkv_progress_id :bigint # title_id :integer not null +# video_blob_id :integer # video_id :integer # # Indexes @@ -23,6 +24,7 @@ # index_disk_titles_on_episode_id (episode_id) # index_disk_titles_on_mkv_progress_id (mkv_progress_id) # index_disk_titles_on_video (video_id) +# index_disk_titles_on_video_blob_id (video_blob_id) # require 'rails_helper' @@ -31,6 +33,7 @@ it { is_expected.to belong_to(:disk).optional(true) } it { is_expected.to belong_to(:episode).optional(true) } it { is_expected.to belong_to(:video).optional(true) } + it { is_expected.to belong_to(:video_blob).optional(true) } end describe 'scopes' do @@ -45,135 +48,4 @@ expect(disk_title.to_label).to eq('#1 Sample Title 1 hour') end end - - describe '#tmp_plex_path' do - context 'when video is a TV' do - let(:episode) { create(:episode) } - let(:disk_title) { create(:disk_title, video: episode.tv, episode:) } - - it 'returns the temporary Plex path for the episode' do - expect(disk_title.tmp_plex_path).to eq(episode.tmp_plex_path) - end - end - - context 'when video is a Movie' do - let(:movie) { create(:movie) } - let(:disk_title) { create(:disk_title, video: movie) } - - it 'returns the temporary Plex path for the movie' do - expect(disk_title.tmp_plex_path).to eq(movie.tmp_plex_path) - end - end - end - - describe '#plex_path' do - before { create(:config_plex) } - - context 'when video is a TV' do - let(:episode) { create(:episode) } - let(:disk_title) { create(:disk_title, video: episode.tv, episode:) } - - it 'returns the Plex path for the episode' do - expect(disk_title.plex_path).to eq(episode.plex_path) - end - end - - context 'when video is a Movie' do - let(:movie) { create(:movie) } - let(:disk_title) { create(:disk_title, video: movie) } - - it 'returns the Plex path for the movie' do - expect(disk_title.plex_path).to eq(movie.plex_path) - end - end - end - - describe '#plex_name' do - context 'when video is a TV' do - let(:episode) { create(:episode) } - let(:disk_title) { create(:disk_title, video: episode.tv, episode:) } - - it 'returns the MKV file name for the episode' do - expect(disk_title.plex_name).to eq(episode.plex_name) - end - end - - context 'when video is a Movie' do - let(:movie) { create(:movie) } - let(:disk_title) { create(:disk_title, video: movie) } - - it 'returns the MKV file name for the movie' do - expect(disk_title.plex_name).to eq(movie.plex_name) - end - end - end - - describe '#tmp_plex_dir' do - context 'when video is a TV' do - let(:episode) { create(:episode) } - let(:disk_title) { create(:disk_title, video: episode.tv, episode:) } - - it 'returns the temporary Plex directory for the episode' do - expect(disk_title.tmp_plex_dir).to eq(episode.tmp_plex_dir) - end - end - - context 'when video is a Movie' do - let(:movie) { create(:movie) } - let(:disk_title) { create(:disk_title, video: movie) } - - it 'returns the temporary Plex directory for the movie' do - expect(disk_title.tmp_plex_dir).to eq(movie.tmp_plex_dir) - end - end - end - - describe '#tmp_plex_path_exists?' do - context 'when video is a TV' do - let(:episode) { create(:episode) } - let(:disk_title) { create(:disk_title, video: episode.tv, episode:) } - - it 'returns whether the temporary Plex path exists for the episode' do - expect(disk_title.tmp_plex_path_exists?).to eq(episode.tmp_plex_path_exists?) - end - end - - context 'when video is a Movie' do - let(:movie) { create(:movie) } - let(:disk_title) { create(:disk_title, video: movie) } - - it 'returns whether the temporary Plex path exists for the movie' do - expect(disk_title.tmp_plex_path_exists?).to eq(movie.tmp_plex_path_exists?) - end - end - end - - describe '#require_movie_or_episode!' do - context 'when video is a Movie' do - let(:movie) { create(:movie) } - let(:disk_title) { create(:disk_title, video: movie) } - - it 'does not raise an error' do - expect { disk_title.require_movie_or_episode! }.not_to raise_error - end - end - - context 'when video is a TV without episode' do - let(:tv) { create(:tv) } - let(:disk_title) { create(:disk_title, video: tv, episode: nil) } - - it 'raises an error' do - expect { disk_title.require_movie_or_episode! }.to raise_error('requires episode or movie to rip') - end - end - - context 'when video is a TV with episode' do - let(:episode) { create(:episode) } - let(:disk_title) { create(:disk_title, video: episode.tv, episode:) } - - it 'does not raise an error' do - expect { disk_title.require_movie_or_episode! }.not_to raise_error - end - end - end end diff --git a/spec/models/movie_spec.rb b/spec/models/movie_spec.rb index e4b82e1b..80b668c1 100644 --- a/spec/models/movie_spec.rb +++ b/spec/models/movie_spec.rb @@ -15,7 +15,6 @@ # poster_path :string # rating :integer default("N/A"), not null # release_date :date -# synced_on :datetime # title :string # type :string # created_at :datetime not null @@ -29,9 +28,6 @@ require 'rails_helper' RSpec.describe Movie do - include_examples 'IsVideo' - - # V describe 'validations' do it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_presence_of(:original_title) } diff --git a/spec/models/tv_spec.rb b/spec/models/tv_spec.rb index 1db5f9fc..cd0bfaf7 100644 --- a/spec/models/tv_spec.rb +++ b/spec/models/tv_spec.rb @@ -15,7 +15,6 @@ # poster_path :string # rating :integer default("N/A"), not null # release_date :date -# synced_on :datetime # title :string # type :string # created_at :datetime not null @@ -29,8 +28,6 @@ require 'rails_helper' RSpec.describe Tv do - include_context 'IsVideo' - describe 'validations' do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:original_name) } diff --git a/spec/models/video_blob_spec.rb b/spec/models/video_blob_spec.rb index cbce124d..4460a9c6 100644 --- a/spec/models/video_blob_spec.rb +++ b/spec/models/video_blob_spec.rb @@ -4,33 +4,38 @@ # # Table name: video_blobs # -# id :integer not null, primary key -# byte_size :bigint not null -# checksum :text -# content_type :string not null -# filename :string not null -# key :string not null -# metadata :text -# optimized :boolean default(FALSE), not null -# service_name :string not null -# created_at :datetime not null -# updated_at :datetime not null -# episode_id :bigint -# video_id :integer +# id :integer not null, primary key +# byte_size :bigint not null +# checksum :text +# content_type :string not null +# extra_type :integer default("feature_films") +# extra_type_number :integer not null +# filename :string not null +# key :string not null +# metadata :text +# optimized :boolean default(FALSE), not null +# uploaded_on :datetime +# created_at :datetime not null +# updated_at :datetime not null +# episode_id :bigint +# video_id :integer # # Indexes # -# index_video_blobs_on_key_and_service_name (key,service_name) UNIQUE -# index_video_blobs_on_video (video_id) +# idx_on_extra_type_number_video_id_extra_type_1978193db6 (extra_type_number,video_id,extra_type) UNIQUE +# index_video_blobs_on_key (key) UNIQUE +# index_video_blobs_on_key_and_service_name (key) UNIQUE +# index_video_blobs_on_video (video_id) # require 'rails_helper' RSpec.describe VideoBlob do - let(:video_blob) { build(:video_blog) } + subject(:video_blob) { build(:video_blob, extra_type_number: 1) } describe 'associations' do it { is_expected.to belong_to(:video).optional(true) } it { is_expected.to belong_to(:episode).optional(true) } + it { is_expected.to have_many(:disk_titles).dependent(:nullify) } end describe 'scopes' do @@ -39,16 +44,77 @@ it { is_expected.to have_scope(:missing_checksum).where(checksum: nil) } end - describe '#tv_show?' do - subject(:tv_show?) { video_blob.tv_show? } + describe 'validations' do + it { is_expected.to validate_presence_of(:key) } + it { is_expected.to validate_uniqueness_of(:key) } + end + + describe '#extra_type_directory' do + subject(:extra_type_directory) { video_blob.send(:extra_type_directory) } + + let(:video_blob) { build_stubbed(:video_blob, extra_type: :behind_the_scenes) } + + it { is_expected.to eq 'Behind The Scenes' } + end + + describe '#plex_path' do + subject(:plex_path) { video_blob.plex_path } before do + config_plex = build_stubbed(:config_plex) allow(Config::Plex).to receive(:newest).and_return(config_plex) end + context 'when feature_films extra type' do + let(:video_blob) { build_stubbed(:video_blob, extra_type: :feature_films, video: movie) } + let(:movie) { build_stubbed(:movie) } + let(:expected_path) do + plex_name = video_blob.send(:plex_name) + "#{Config::Plex.newest.settings_movie_path}/#{plex_name}/#{plex_name}.mkv" + end + + it { is_expected.to eq Pathname.new(expected_path) } + end + + context 'when not feature_films extra type' do + let(:video_blob) { build_stubbed(:video_blob, extra_type: :behind_the_scenes, video: movie) } + let(:movie) { build_stubbed(:movie) } + let(:expected_path) do + "#{Config::Plex.newest.settings_movie_path}/#{movie.plex_name}/Behind The Scenes/Behind The Scenes #1.mkv" + end + + it { is_expected.to eq Pathname.new(expected_path) } + end + + context 'when not feature_films extra type & tv show' do + let(:video_blob) { build_stubbed(:video_blob, extra_type: :behind_the_scenes, video: tv) } + let(:tv) { build_stubbed(:tv) } + let(:expected_path) do + "#{Config::Plex.newest.settings_tv_path}/#{tv.plex_name}/Behind The Scenes/Behind The Scenes #1.mkv" + end + + it { is_expected.to eq Pathname.new(expected_path) } + end + end + + describe '#set_extra_type_number' do + context 'when using movies types' do + let!(:video_blob_a) { create(:video_blob, video: movie, extra_type: :feature_films) } + let!(:video_blob_b) { create(:video_blob, video: movie, extra_type: :feature_films) } + let(:movie) { create(:movie) } + + it { expect(video_blob_a.extra_type_number).to eq 1 } + it { expect(video_blob_b.extra_type_number).to eq 2 } + end + end + + describe '#tv_show?' do + subject(:tv_show?) { video_blob.tv_show? } + + before { allow(Config::Plex).to receive(:newest).and_return(config_plex) } + context 'when path does not start with tv show path' do let(:config_plex) { build_stubbed(:config_plex, settings_tv_path: '/Media/Tv Show') } - let(:video_blob) { build_stubbed(:video_blob, key: '/Tv Show') } it { is_expected.to be(false) } @@ -56,7 +122,6 @@ context 'when path does start with tv show path' do let(:config_plex) { build_stubbed(:config_plex, settings_tv_path: '/Media/Tv Show') } - let(:video_blob) { build_stubbed(:video_blob, key: '/Media/Tv Show') } it { is_expected.to be(true) } @@ -112,5 +177,21 @@ it { is_expected.to be(false) } end + + context 'when the video is tv' do + let(:config_plex) { build_stubbed(:config_plex, settings_movie_path: '') } + let(:video_blob) { build_stubbed(:video_blob, key: '/Movie', video:) } + let(:video) { build_stubbed(:tv) } + + it { is_expected.to be(false) } + end + + context 'when the video is movie' do + let(:config_plex) { build_stubbed(:config_plex, settings_movie_path: '') } + let(:video_blob) { build_stubbed(:video_blob, key: '/Movie', video:) } + let(:video) { build_stubbed(:movie) } + + it { is_expected.to be(true) } + end end end diff --git a/spec/models/video_spec.rb b/spec/models/video_spec.rb index f92e100f..2cc5de92 100644 --- a/spec/models/video_spec.rb +++ b/spec/models/video_spec.rb @@ -15,7 +15,6 @@ # poster_path :string # rating :integer default("N/A"), not null # release_date :date -# synced_on :datetime # title :string # type :string # created_at :datetime not null @@ -31,7 +30,7 @@ RSpec.describe Video do describe 'associations' do it { is_expected.to have_many(:disk_titles).dependent(:nullify) } - it { is_expected.to have_many(:video_blobs).dependent(:destroy) } + it { is_expected.to have_many(:video_blobs).dependent(:nullify) } it { is_expected.to have_many(:optimized_video_blobs).dependent(:destroy) } end end diff --git a/spec/requests/movies_request_spec.rb b/spec/requests/movies_request_spec.rb index d72d67f9..aa6aea98 100644 --- a/spec/requests/movies_request_spec.rb +++ b/spec/requests/movies_request_spec.rb @@ -13,9 +13,11 @@ describe '/:id/rip' do let!(:movie) { create(:movie) } let!(:disk_title) { create(:disk_title) } + let(:disk_id) { disk_title.disk.id } it 'renders a successful response' do - post rip_movie_url(movie, disk_title_id: disk_title.id) + post rip_movie_url(movie), + params: { disk_id:, movies: [{ extra_type: 'feature_film', disk_title_id: disk_title.id }] } expect(Job.count).to eq 1 expect(response).to have_http_status :found diff --git a/spec/services/create_mkv_service_spec.rb b/spec/services/create_mkv_service_spec.rb index 2eda610d..e712d66a 100644 --- a/spec/services/create_mkv_service_spec.rb +++ b/spec/services/create_mkv_service_spec.rb @@ -4,7 +4,6 @@ RSpec.describe CreateMkvService do let(:service) { described_class.new(disk_title:) } - let(:disk_title) { build_stubbed(:disk_title) } before { create(:config_make_mkv) } @@ -12,12 +11,14 @@ subject(:call) { service.call } context 'when the disk title is valid' do - let(:disk_title) { build_stubbed(:disk_title, :with_movie) } + let(:disk_title) { build_stubbed(:disk_title, video: movie, video_blob:) } + let(:video_blob) { build_stubbed(:video_blob, video: movie) } + let(:movie) { build_stubbed(:movie) } before { allow(service).to receive(:cmd).and_return('ls /not-a-real-folder') } it 'responds with a result object' do - expect(call).to eq(described_class::Result.new(disk_title.tmp_plex_path, false)) + expect(call).to eq(described_class::Result.new(video_blob.tmp_plex_path, false)) end end end diff --git a/spec/support/shared_example/is_video.rb b/spec/support/shared_example/is_video.rb deleted file mode 100644 index 834304b6..00000000 --- a/spec/support/shared_example/is_video.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'IsVideo' do |_parameter| - let(:model_class) { described_class.model_name.singular.to_sym } - let(:model) { build_stubbed(model_class) } - - describe '#release_or_air_date', :freeze do - subject(:release_or_air_date) { video.release_or_air_date } - - let(:expected_date) { Time.zone.today } - let(:unexpected_date) { 2.days.ago } - - context 'when release_date is present' do - let(:video) { build_stubbed(model_class, release_date: expected_date, episode_first_air_date: nil) } - - it { is_expected.to eq expected_date } - end - - context 'when episode_first_air_date is present' do - let(:video) { build_stubbed(model_class, release_date: nil, episode_first_air_date: expected_date) } - - it { is_expected.to eq expected_date } - end - - context 'when both release_date & episode_first_air_date is present' do - let(:video) do - build_stubbed( - model_class, - release_date: expected_date, - episode_first_air_date: unexpected_date - ) - end - - it { is_expected.to eq expected_date } - end - end -end diff --git a/spec/workers/upload_worker_spec.rb b/spec/workers/upload_worker_spec.rb index a7a1a843..a80dac6b 100644 --- a/spec/workers/upload_worker_spec.rb +++ b/spec/workers/upload_worker_spec.rb @@ -3,11 +3,12 @@ require 'rails_helper' RSpec.describe UploadWorker, type: :worker do - subject(:worker) { described_class.new(disk_title_id:, job:) } + subject(:worker) { described_class.new(video_blob_id:, job:) } - let(:disk_title) { create(:disk_title, video: movie) } + let(:disk_title) { create(:disk_title, video: movie, video_blob:) } + let(:video_blob) { create(:video_blob, video: movie) } let(:movie) { create(:movie) } - let(:disk_title_id) { disk_title.id } + let(:video_blob_id) { video_blob.id } let(:job) { create(:job) } let(:stub_ftp) { instance_double(Net::FTP) } @@ -18,7 +19,7 @@ allow(stub_ftp).to receive(:mkdir) allow(stub_ftp).to receive(:putbinaryfile) - FileUtils.mkdir_p(disk_title.tmp_plex_path) + FileUtils.mkdir_p(video_blob.tmp_plex_path) end describe '#perform' do