diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index a0a51ee8..9fdfddef 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-08-29 18:05:38 UTC using RuboCop version 1.65.1.
+# on 2024-09-03 17:09:41 UTC using RuboCop version 1.65.1.
# 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
@@ -97,10 +97,11 @@ RSpec/ExampleLength:
Exclude:
- 'spec/models/video_blob_spec.rb'
-# Offense count: 1
+# Offense count: 2
# Configuration parameters: Max.
RSpec/MultipleExpectations:
Exclude:
+ - 'spec/listeners/disk_listener_spec.rb'
- 'spec/requests/movies_request_spec.rb'
# Offense count: 1
@@ -116,6 +117,11 @@ RSpec/NamedSubject:
Exclude:
- 'spec/models/video_blob_spec.rb'
+# Offense count: 1
+RSpec/SubjectStub:
+ Exclude:
+ - 'spec/listeners/disk_listener_spec.rb'
+
# Offense count: 12
Rails/I18nLocaleTexts:
Exclude:
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index c66c0871..775240d5 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -20,6 +20,7 @@
@import "checkbox_toggle";
@import "bg_process";
@import "autocomplete";
+@import "movie";
// @import "./custom";
// app/javascript/packs/stylesheets/_custom.scss
@@ -33,6 +34,15 @@ body {
body {
font-family: "Lucida Console", Monaco, monospace;
+
+ main {
+ padding: 1em;
+ padding-bottom: 20em;
+ }
+}
+
+.nav-link {
+ padding: 0.5em;
}
.height-100 {
@@ -47,10 +57,8 @@ body {
height: 60px;
}
-.sticky-element {
- position: -webkit-sticky;
- position: sticky;
- top: 0;
+.margin-auto {
+ margin: auto;
}
// Console for basic shell output
@@ -76,6 +84,31 @@ body {
padding: 0.5rem;
}
+.certification-rating {
+ align-content: center;
+ align-items: center;
+ border-radius: 2px;
+ border: 1px solid rgba(255, 255, 255, 0.6);
+ line-height: 1;
+ margin-right: 7px;
+ padding: .06em 4px .06em;
+ white-space: nowrap;
+}
+
+.cast {
+ display: flex;
+ overflow-x: scroll;
+
+ .cast-member {
+ .img-poster {
+ padding-bottom: 1em;
+ }
+
+ width: 200px;
+ }
+}
+
+
.h-90 {
height: 90%;
}
@@ -96,6 +129,10 @@ body {
display: inline-grid;
}
+.hidden {
+ display: none;
+}
+
.overflow-scroll-y {
overflow-y: scroll;
}
@@ -115,6 +152,7 @@ body {
.img-poster {
height: 300px;
width: 200px;
+ display: inline-block;
}
.width-225 {
diff --git a/app/assets/stylesheets/bg_process.scss b/app/assets/stylesheets/bg_process.scss
index f9c402eb..044b3c31 100644
--- a/app/assets/stylesheets/bg_process.scss
+++ b/app/assets/stylesheets/bg_process.scss
@@ -1,3 +1,11 @@
+#bg-processes.fixed {
+ max-width: 23%;
+ position: fixed;
+ right: 1em;
+ bottom: 5em;
+ z-index: 1000;
+}
+
.bg-process {
--bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);
--bs-toast-border-color: var(--bs-border-color-translucent);
@@ -48,7 +56,6 @@
}
-
.job {
--bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);
--bs-toast-border-color: var(--bs-border-color-translucent);
@@ -97,3 +104,18 @@
}
}
+
+#bg-processes.compact {
+ .bg-process {
+ margin: 0px;
+ border-radius: 0;
+ border: 0.2em solid var(--bs-toast-border-color);
+
+ .header {
+ display: none;
+ padding: 0.5em;
+ }
+
+ }
+
+}
diff --git a/app/assets/stylesheets/minimal.scss b/app/assets/stylesheets/minimal.scss
index 9961ce7b..dbdf1fa7 100644
--- a/app/assets/stylesheets/minimal.scss
+++ b/app/assets/stylesheets/minimal.scss
@@ -33,34 +33,34 @@
}
input {
- border-radius: 2px 2px 0px 0px;
background-color: transparent;
+ border-color: transparent;
+ border-radius: 2px 2px 0px 0px;
border: 0;
color: $white;
display: block;
font-family: "Lucida Console", Monaco, monospace;
font-size: 20px;
height: 35px;
- padding-bottom: 3px;
line-height: 1.2;
outline: none;
+ padding-bottom: 3px;
width: 100%;
- border-color: transparent;
}
label {
display: block;
+ font-size: 22px;
height: 100%;
left: 0;
+ overflow: hidden;
pointer-events: none;
position: absolute;
- width: 100%;
- font-size: 22px;
+ text-overflow: ellipsis;
top: 0px;
transition: all .4s;
- overflow: hidden;
- text-overflow: ellipsis;
white-space: nowrap;
+ width: 100%;
}
input:focus + label, input.has-val + label {
diff --git a/app/assets/stylesheets/movie.scss b/app/assets/stylesheets/movie.scss
new file mode 100644
index 00000000..cf816cc0
--- /dev/null
+++ b/app/assets/stylesheets/movie.scss
@@ -0,0 +1,3 @@
+.movie-facts {
+ color: $light-gray
+}
diff --git a/app/components/cast_component.html.erb b/app/components/cast_component.html.erb
new file mode 100644
index 00000000..50622fb0
--- /dev/null
+++ b/app/components/cast_component.html.erb
@@ -0,0 +1,11 @@
+
+ <% cast.each do |cast| %>
+
+ <%= imdb_image_tag cast[:profile_path], width: 150, klass: 'img-poster' %>
+
+
<%= cast[:name] %>
+
<%= cast[:character] %>
+
+
+ <% end %>
+
diff --git a/app/components/cast_component.rb b/app/components/cast_component.rb
new file mode 100644
index 00000000..bc937f85
--- /dev/null
+++ b/app/components/cast_component.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class CastComponent < ViewComponent::Base
+ include ImdbHelper
+ Member = Types::Hash.schema(
+ character: Types::String,
+ name: Types::String,
+ profile_path: Types::String.optional
+ ).with_key_transform(&:to_sym)
+ Cast = Types::Coercible::Array.of(Member)
+
+ extend Dry::Initializer
+ option :video, Types.Instance(::Video)
+
+ strip_trailing_whitespace
+
+ def cast
+ return [] if video.credits.nil?
+
+ @cast ||= Cast[video.credits['cast']]
+ end
+
+ def render?
+ cast.any?
+ end
+end
diff --git a/app/components/load_disk_process_component.html.erb b/app/components/load_disk_process_component.html.erb
index 0efb4fa6..82e29209 100644
--- a/app/components/load_disk_process_component.html.erb
+++ b/app/components/load_disk_process_component.html.erb
@@ -1,25 +1,29 @@
-<%= render ProcessComponent.new dom_id: dom_id do |c| %>
- <%= c.with_title do %>
- Disk Status
- <% end %>
- <%= c.with_body do %>
- <% if job_active? && disks_loading.any? %>
- <% disks_loading.each do |disk| %>
-
- Loading <%= link_to disk.name, the_movie_dbs_path(search: { query: disk.name.titleize }) %> ... <%= link_to 'View Details', job_path(job) %>
-
- <%= Array.wrap(job.metadata['message']).last %>
- <% end %>
- <% elsif disks_not_ejected.any? %>
- <% disks_not_ejected.each do |disk| %>
-
- <%= link_to disk.name, the_movie_dbs_path(search: { query: disk.name.titleize }) %> is ready to be ripped. <%= link_to 'eject', eject_disk_path(disk), data: { turbo_method: :post } %>
-
+<% if hidden? %>
+
+<% else %>
+ <%= render ProcessComponent.new dom_id: dom_id do |c| %>
+ <%= c.with_title do %>
+ Disk Status
+ <% end %>
+ <%= c.with_body do %>
+ <% if job_active? && disks_loading.any? %>
+ <% disks_loading.each do |disk| %>
+
+ Loading <%= link_to disk.name, the_movie_dbs_path(search: { query: disk.name.titleize }) %> ... <%= link_to 'View Details', job_path(job) %>
+
+ <%= Array.wrap(job.metadata['message']).last %>
+ <% end %>
+ <% elsif disks_not_ejected.any? %>
+ <% disks_not_ejected.each do |disk| %>
+
+ <%= link_to disk.name, the_movie_dbs_path(search: { query: disk.name.titleize }) %> is ready to be ripped. <%= link_to 'eject', eject_disk_path(disk), data: { turbo_method: :post } %>
+
+ <% end %>
+ <% elsif job_active? %>
+ Loading disk info ...<%= link_to 'View Details', job_path(job) %>
+ <% else %>
+ No disks found - insert disk to continue
<% end %>
- <% elsif job_active? %>
- Loading disk info ...<%= link_to 'View Details', job_path(job) %>
- <% else %>
- No disks found - insert disk to continue
<% end %>
<% end %>
<% end %>
diff --git a/app/components/load_disk_process_component.rb b/app/components/load_disk_process_component.rb
index 5a0ccc07..2021d5ca 100644
--- a/app/components/load_disk_process_component.rb
+++ b/app/components/load_disk_process_component.rb
@@ -7,6 +7,14 @@ def self.job
Job.sort_by_created_at.active.find_by(name: 'LoadDiskWorker')
end
+ def self.show?
+ job&.active?
+ end
+
+ def hidden?
+ !self.class.show? && disks_not_ejected.empty?
+ end
+
def job_active?
job&.active?
end
diff --git a/app/components/movie_title_table_component.html.erb b/app/components/movie_title_table_component.html.erb
index d518b1dd..564bee95 100644
--- a/app/components/movie_title_table_component.html.erb
+++ b/app/components/movie_title_table_component.html.erb
@@ -1,6 +1,5 @@
<% if disks.any? %>
<%= simple_form_for :movies, url: rip_movie_path(movie) do |f| %>
- <% disk = disks.first %>
<%= hidden_field_tag :disk_id, disk&.id %>
@@ -15,16 +14,15 @@
- <% disk.disk_titles.sort_by { |d| movie.runtime_range.include?(d.duration) ? 0 : 1 }.each do |disk_title| %>
+ <% disk_titles_with_info.each do |info| %>
+ <% disk_title = info.disk_title %>
<%= hidden_field_tag "movies[][disk_title_id]", disk_title.id %>
- <% in_range = movie.runtime_range.include?(disk_title.duration) %>
- <% text_class = 'text-primary-emphasis' if in_range %>
- <% text_class = 'text-success-emphasis' if movie.video_blobs.any? && in_range %>
+ <% text_class = 'text-primary-emphasis' if info.within_range %>
<%= disk_title.id %> |
- <% if movie.ripped_disk_titles.any? { _1.name == disk_title.name } %>
+ <% if info.ripped? %>
<%= icon('square-check') %>
<% else %>
<%= icon('square') %>
@@ -33,7 +31,7 @@
|
- <% if movie.ripped_disk_titles.find { _1.name == disk_title.name }&.video_blob&.uploaded? %>
+ <% if info.uploaded? %>
<%= icon('square-check') %>
<% else %>
<%= icon('square') %>
@@ -47,7 +45,9 @@
(<%= disk_title.video&.title %>)
<% end %>
|
- <%= distance_of_time_in_words(disk_title.duration.seconds) %> |
+
+ <%= 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 %>
@@ -55,16 +55,7 @@
<% end %>
|
- <% if movie.ripped_disk_titles.find { _1.name == disk_title.name }&.video_blob&.uploaded? %>
- <%= select_tag "movies[][extra_type]", options_for_select(VideoBlob.extra_types.keys, nil), prompt: "Don't Rip" %>
- <% elsif in_range && feature_film_selected? %>
- <%= select_tag "movies[][extra_type]", options_for_select(VideoBlob.extra_types.keys, 'shorts'), prompt: "Don't Rip" %>
- <% elsif in_range %>
- <% feature_film_selected! %>
- <%= select_tag "movies[][extra_type]", options_for_select(VideoBlob.extra_types.keys, 'feature_films'), prompt: "Don't Rip" %>
- <% else %>
- <%= select_tag "movies[][extra_type]", options_for_select(VideoBlob.extra_types.keys, 'other'), prompt: "Don't Rip" %>
- <% end %>
+ <%= select_tag "movies[][extra_type]", options_for_select(VideoBlob.extra_types.keys, info.extra_type), prompt: "Don't Rip" %>
|
<% end %>
@@ -74,8 +65,12 @@
<% end %>
-<% elsif 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 %>
- Sorry no Disk is currently loaded or could be detected. Refresh page if your seeing this warning but know a disk is present.
+
+ <% if movie.auto_start %>
+
Will Start ripping movie as soon as the disk is ready.
+ <% else %>
+ <%= link_to 'Auto Start', auto_start_movie_path(movie), class: 'btn btn-primary', data: { turbo_method: :post } %>
+ <% end %>
+
<% end %>
diff --git a/app/components/movie_title_table_component.rb b/app/components/movie_title_table_component.rb
index 01cd2cb9..6067c252 100644
--- a/app/components/movie_title_table_component.rb
+++ b/app/components/movie_title_table_component.rb
@@ -11,6 +11,18 @@ def dom_id
"#{self.class.name.parameterize}-#{movie.id}"
end
+ def disks_loading
+ @disks_loading ||= Disk.loading
+ end
+
+ def disk_titles_with_info
+ MovieDiskTitleSelectorService.call(movie:, disk:)
+ end
+
+ def disk
+ @disk ||= disks.first
+ end
+
def free_disk_space
@free_disk_space ||= stats.block_size * stats.blocks_available
end
diff --git a/app/components/process_component.html.erb b/app/components/process_component.html.erb
index 5e8a183b..112bdd6d 100644
--- a/app/components/process_component.html.erb
+++ b/app/components/process_component.html.erb
@@ -1,9 +1,9 @@
<% if link %>
-
<%= body %>
-
<%= link %>
+
<%= body %>
+
<%= link %>
<% else %>
-
<%= body %>
+
<%= body %>
<% end %>
diff --git a/app/components/rip_process_component.html.erb b/app/components/rip_process_component.html.erb
index 1f6515a1..9adb7897 100644
--- a/app/components/rip_process_component.html.erb
+++ b/app/components/rip_process_component.html.erb
@@ -1,4 +1,4 @@
-<% if hide %>
+<% if hidden? %>
<% else %>
<%= render ProcessComponent.new dom_id: dom_id do |c| %>
@@ -21,8 +21,13 @@
<% c.with_link { link_to 'View Details', job_path(job) } %>
<% else %>
<% c.with_body do %>
- Nothing is being ripped to <%= ftp_host %>
+ <% if auto_start_video && auto_start_video.is_a?(Movie) %>
+ <%= link_to 'Cancel', cancel_auto_start_movie_path(auto_start_video), data: { turbo_method: :post } %> | <%= link_to auto_start_video.title, movie_path(auto_start_video) %> will start ripping
+ <% else %>
+ Nothing is being ripped to <%= ftp_host %>
+ <% end %>
<% end %>
+
<% end %>
<% end %>
<% end %>
diff --git a/app/components/rip_process_component.rb b/app/components/rip_process_component.rb
index 37594078..cf045795 100644
--- a/app/components/rip_process_component.rb
+++ b/app/components/rip_process_component.rb
@@ -5,6 +5,14 @@ def self.job
Job.sort_by_created_at.active.find_by(name: 'RipWorker')
end
+ def self.show?
+ Video.auto_start.any? || job&.active?
+ end
+
+ def hidden?
+ !self.class.show?
+ end
+
def job
self.class.job
end
@@ -21,6 +29,10 @@ def job_active?
job&.active?
end
+ def auto_start_video
+ @auto_start_video ||= Video.auto_start.first
+ end
+
def ftp_host
Config::Plex.newest.settings_ftp_host
end
diff --git a/app/components/scan_plex_process_component.html.erb b/app/components/scan_plex_process_component.html.erb
index 57187282..41b3786f 100644
--- a/app/components/scan_plex_process_component.html.erb
+++ b/app/components/scan_plex_process_component.html.erb
@@ -1,17 +1,21 @@
-<%= render ProcessComponent.new dom_id: dom_id do |c| %>
- <%= c.with_title do %>
- Scan Plex Status
- <% end %>
- <%= c.with_body do %>
- <% if job_active? %>
- <%= render ProgressBarComponent.new(
- completed: completed,
- status: status,
- message: "Scanning #{ftp_host} for movies and tv shows",
- )
- %>
- <% else %>
- Done! you have a total of <%= pluralize(Movie.count, 'movie') %> & <%= pluralize(Tv.count, 'tv show') %> on plex.
+<% if hidden? %>
+
+<% else %>
+ <%= render ProcessComponent.new dom_id: dom_id do |c| %>
+ <%= c.with_title do %>
+ Scan Plex Status
+ <% end %>
+ <%= c.with_body do %>
+ <% if job_active? %>
+ <%= render ProgressBarComponent.new(
+ completed: completed,
+ status: status,
+ message: "Scanning #{ftp_host} for movies and tv shows",
+ )
+ %>
+ <% else %>
+ Done! you have a total of <%= pluralize(Movie.count, 'movie') %> & <%= pluralize(Tv.count, 'tv show') %> on plex.
+ <% end %>
<% end %>
<% end %>
<% end %>
diff --git a/app/components/scan_plex_process_component.rb b/app/components/scan_plex_process_component.rb
index c0707f90..36333393 100644
--- a/app/components/scan_plex_process_component.rb
+++ b/app/components/scan_plex_process_component.rb
@@ -7,6 +7,14 @@ def self.job
Job.sort_by_created_at.active.find_by(name: 'ScanPlexWorker')
end
+ def self.show?
+ job&.active? || false
+ end
+
+ def hidden?
+ !self.class.show?
+ end
+
def completed
job.metadata['completed'].presence || 0.0
end
diff --git a/app/components/search_component.html.erb b/app/components/search_component.html.erb
index 5990a4af..0d5c22ab 100644
--- a/app/components/search_component.html.erb
+++ b/app/components/search_component.html.erb
@@ -1,8 +1,8 @@
-<%= simple_form_for search_service, as: :search, url: the_movie_dbs_path, method: :get, html: { class: 'row m-1 pb-4' }, data: { turbo_frame: 'videos', turbo_action: 'replace' } do |f| %>
+<%= simple_form_for search_service, as: :search, url: the_movie_dbs_path, method: :get, html: { class: 'row pb-4', style: "margin: auto; width: 50%;" }, data: { turbo_frame: 'videos', turbo_action: 'replace', controller: 'submit-on-keyup' } do |f| %>
- <%= f.input :query, label: 'Search for TV or Movie', autofocus: true, required: false %>
+ <%= f.input :query, label: 'Search for TV or Movie', autofocus: true, required: false, input_html: { data: { action: "keyup->submit-on-keyup#submitWithDebounce", submit_on_keyup_target: :input } } %>
-
+
<%= f.button :submit, 'Search', data: { disable_with: 'Searching' } %>
<% end %>
diff --git a/app/components/upload_process_component.html.erb b/app/components/upload_process_component.html.erb
index 39630a43..34b6a864 100644
--- a/app/components/upload_process_component.html.erb
+++ b/app/components/upload_process_component.html.erb
@@ -1,37 +1,41 @@
-<%= render ProcessComponent.new dom_id: dom_id do |c| %>
- <%= c.with_title do %>
- Upload Status
- <% end %>
- <%= c.with_body do %>
- <% if uploadable_video_blobs.any? || uploaded_recently_video_blobs.any? %>
- <% uploaded_recently_video_blobs.each do |blob| %>
- <%=
- render(
- ProgressBarComponent.new(
- show_percentage: false,
- status: :success,
- completed: 100,
- message: "Uploaded #{blob.title}"
+<% if hidden? %>
+
+<% else %>
+ <%= render ProcessComponent.new dom_id: dom_id, hidden: hidden? do |c| %>
+ <%= c.with_title do %>
+ Upload Status
+ <% end %>
+ <%= c.with_body do %>
+ <% if uploadable_video_blobs.any? || uploaded_recently_video_blobs.any? %>
+ <% uploaded_recently_video_blobs.each do |blob| %>
+ <%=
+ render(
+ ProgressBarComponent.new(
+ show_percentage: false,
+ status: :success,
+ completed: 100,
+ message: "Uploaded #{blob.title}"
+ )
)
- )
- %>
- <% end %>
- <% uploadable_video_blobs.each do |blob| %>
- <%=
- video_blob_job = find_job_by_video_blob(blob)
- render(
- ProgressBarComponent.new(
- show_percentage: video_blob_job.present?,
- status: :info,
- completed: percentage(video_blob_job&.completed, blob.byte_size),
- message: video_blob_job ? "Uploading #{blob.title}" : "Pending upload #{blob.title}",
- eta: (eta(video_blob_job, blob) if video_blob_job)
+ %>
+ <% end %>
+ <% uploadable_video_blobs.each do |blob| %>
+ <%=
+ video_blob_job = find_job_by_video_blob(blob)
+ render(
+ ProgressBarComponent.new(
+ show_percentage: video_blob_job.present?,
+ status: :info,
+ completed: percentage(video_blob_job&.completed, blob.byte_size),
+ message: video_blob_job ? "Uploading #{blob.title}" : "Pending upload #{blob.title}",
+ eta: (eta(video_blob_job, blob) if video_blob_job)
+ )
)
- )
- %>
+ %>
+ <% end %>
+ <% else %>
+
Nothing is being uploaded to <%= ftp_host %>
<% end %>
- <% else %>
-
Nothing is being uploaded to <%= ftp_host %>
<% end %>
<% end %>
<% end %>
diff --git a/app/components/upload_process_component.rb b/app/components/upload_process_component.rb
index c33f45cd..e2fe463f 100644
--- a/app/components/upload_process_component.rb
+++ b/app/components/upload_process_component.rb
@@ -5,6 +5,14 @@ def self.job
Job.sort_by_created_at.active.find_by(name: 'UploadWorker')
end
+ def self.show?
+ VideoBlob.uploaded_recently.any? || VideoBlob.uploadable.any?
+ end
+
+ def hidden?
+ !self.class.show?
+ end
+
def dom_id
'upload-process-component'
end
diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb
index fb92c04d..622b2112 100644
--- a/app/controllers/movies_controller.rb
+++ b/app/controllers/movies_controller.rb
@@ -26,6 +26,23 @@ def rip
redirect_to job_path(job)
end
+ # Call this method to auto start ripping the disk as soon as it the disk title is ready
+ # It will auto select disk titles for you rather then you selecting them
+ def auto_start
+ movie = Movie.find(params[:id])
+ Video.auto_start.in_batches { _1.update(auto_start: false) }
+ movie.update!(auto_start: true)
+ flash[:notice] = "Once disk is loaded is ready we will start processings #{movie.title}"
+ redirect_to movie_path(movie)
+ end
+
+ def cancel_auto_start
+ movie = Movie.find(params[:id])
+ movie.update!(auto_start: false)
+ flash[:notice] = "Removed #{movie.title} from auto start"
+ redirect_back_or_to :root
+ end
+
private
def rip_disk_titles(disk, movie)
diff --git a/app/javascript/channels/broadcast_channel.js b/app/javascript/channels/broadcast_channel.js
index 4550a6aa..882675b7 100644
--- a/app/javascript/channels/broadcast_channel.js
+++ b/app/javascript/channels/broadcast_channel.js
@@ -9,5 +9,24 @@ consumer.subscriptions.create('BroadcastChannel', {
// },
received(data) {
if (data.cableReady) CableReady.perform(data.operations)
+
+ updateSidebarVisibility()
}
})
+
+
+function updateSidebarVisibility() {
+ const sidebar = document.getElementById('sidebar');
+ // Check if there are any visible children within the sidebar
+ const hasVisibleChildren = Array.from(sidebar.children).some(child => {
+ return child.offsetWidth > 0 && child.offsetHeight > 0 && !child.classList.contains('hidden');
+ });
+
+ if (hasVisibleChildren) {
+ // If there are visible children, remove the 'hidden' class if it exists
+ sidebar.classList.remove('hidden');
+ } else {
+ // If no visible children, add the 'hidden' class
+ sidebar.classList.add('hidden');
+ }
+}
diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js
index 523d37bd..43ded407 100644
--- a/app/javascript/controllers/index.js
+++ b/app/javascript/controllers/index.js
@@ -12,3 +12,6 @@ application.register("pagination", PaginationController)
import PlexController from "./plex_controller"
application.register("plex", PlexController)
+
+import SubmitOnKeyupController from "./submit_on_keyup_controller"
+application.register("submit-on-keyup", SubmitOnKeyupController)
diff --git a/app/javascript/controllers/pagination_controller.js b/app/javascript/controllers/pagination_controller.js
index 867e3839..086e9695 100644
--- a/app/javascript/controllers/pagination_controller.js
+++ b/app/javascript/controllers/pagination_controller.js
@@ -5,10 +5,10 @@ export default class extends Controller {
connect() {
if (!this.hasShowMoreTarget || this.showMoreTarget.dataset.disableWith === undefined) return
- console.log("loading");
window.addEventListener("scroll", () => {
if (this.showMoreVisable()) this.showMoreTarget.click();
});
+ if (this.showMoreVisable()) this.showMoreTarget.click();
}
showMoreVisable() {
diff --git a/app/javascript/controllers/submit_on_keyup_controller.js b/app/javascript/controllers/submit_on_keyup_controller.js
new file mode 100644
index 00000000..53c0d33b
--- /dev/null
+++ b/app/javascript/controllers/submit_on_keyup_controller.js
@@ -0,0 +1,22 @@
+import { Controller } from "@hotwired/stimulus"
+import debounce from "lodash/debounce";
+
+// Connects to data-controller="submit-on-keyup"
+export default class extends Controller {
+ static targets = ["input"]
+
+ connect() {
+ this.lastSubmittedValue = this.inputTarget.value; // Initialize with the current input value
+ this.submitWithDebounce = debounce(this.submitWithDebounce.bind(this), 300);
+ }
+
+ submitWithDebounce(event) {
+ event.preventDefault();
+
+ // Only submit if the current input value is different from the last submitted value
+ if (this.inputTarget.value !== this.lastSubmittedValue) {
+ this.lastSubmittedValue = this.inputTarget.value; // Update the last submitted value
+ this.element.requestSubmit();
+ }
+ }
+}
diff --git a/app/listeners/disk_listener.rb b/app/listeners/disk_listener.rb
index 32a19624..511dfc1d 100644
--- a/app/listeners/disk_listener.rb
+++ b/app/listeners/disk_listener.rb
@@ -6,36 +6,67 @@ class DiskListener
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::DateHelper
include SlackUtility
+ include ActionView::Helpers::UrlHelper
+ delegate :job_url, to: 'Rails.application.routes.url_helpers'
delegate :render, to: :ApplicationController
def disk_ejecting(disk)
- cable_broadcast("Ejecting Disk #{disk.name}")
+ cable_broadcast(message: "Ejecting Disk #{disk.name}")
end
def disk_ejected(disk)
message = "Disk Ejected #{disk.name} - Ready for new disk"
notify_slack(message)
- cable_broadcast(message)
+ cable_broadcast(message:)
end
def disk_eject_failed(disk, exception)
message = "Failed to eject #{disk.name} - #{exception.message}"
notify_slack(message)
- cable_broadcast(message)
+ cable_broadcast(message:)
end
- def disk_loading
+ def disk_loading(_)
cable_broadcast
end
- def disk_loaded
- reload_page!
+ def disk_loaded(disk)
+ return reload_page! unless (video = Video.auto_start.first)
+
+ info_disk_titles = rip_disk_titles(disk, video)
+ return reload_page! if info_disk_titles.empty?
+
+ job = RipWorker.perform_async(
+ disk_id: disk.id,
+ disk_title_ids: info_disk_titles.map { _1.disk_title.id },
+ extra_types: info_disk_titles.map(&:extra_type)
+ )
+ if job
+ video.update!(auto_start: false)
+ redirect_to_job(job)
+ else
+ reload_page!
+ end
end
private
- def cable_broadcast(message = nil)
+ def redirect_to_job(job)
+ cable_ready[BroadcastChannel.channel_name].redirect_to(url: job_url(job))
+ cable_ready.broadcast
+ end
+
+ def rip_disk_titles(disk, video)
+ return [] unless video.is_a?(Movie)
+
+ MovieDiskTitleSelectorService.call(movie: video, disk:).select do |info|
+ info.disk_title.update!(video:) if info.extra_type.present?
+ info.extra_type.present?
+ end
+ end
+
+ def cable_broadcast(message: nil)
component = LoadDiskProcessComponent.new(message:)
cable_ready[BroadcastChannel.channel_name].morph(
diff --git a/app/listeners/mkv_progress_listener.rb b/app/listeners/mkv_progress_listener.rb
index 1f21d72c..6299a950 100644
--- a/app/listeners/mkv_progress_listener.rb
+++ b/app/listeners/mkv_progress_listener.rb
@@ -90,7 +90,7 @@ def update_job!
return if next_update.future?
update_progress_bar
- job.save!
+ job.save! if job.changed?
@next_update = nil
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 2cb72f9f..c061154a 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -3,6 +3,7 @@
class ApplicationRecord < ActiveRecord::Base
include CableReady::Broadcaster
+ scope :newest, -> { order(created_at: :desc) }
self.abstract_class = true
def unmark_for_destruction
diff --git a/app/models/movie.rb b/app/models/movie.rb
index 9708d192..66662611 100644
--- a/app/models/movie.rb
+++ b/app/models/movie.rb
@@ -5,6 +5,7 @@
# Table name: videos
#
# id :integer not null, primary key
+# auto_start :boolean default(FALSE), not null
# backdrop_path :string
# episode_distribution_runtime :string
# episode_first_air_date :date
diff --git a/app/models/tv.rb b/app/models/tv.rb
index 8abcaa84..fa623c80 100644
--- a/app/models/tv.rb
+++ b/app/models/tv.rb
@@ -5,6 +5,7 @@
# Table name: videos
#
# id :integer not null, primary key
+# auto_start :boolean default(FALSE), not null
# backdrop_path :string
# episode_distribution_runtime :string
# episode_first_air_date :date
diff --git a/app/models/video.rb b/app/models/video.rb
index 0620a6c0..511dd9ee 100644
--- a/app/models/video.rb
+++ b/app/models/video.rb
@@ -5,6 +5,7 @@
# Table name: videos
#
# id :integer not null, primary key
+# auto_start :boolean default(FALSE), not null
# backdrop_path :string
# episode_distribution_runtime :string
# episode_first_air_date :date
@@ -39,6 +40,8 @@ class Video < ApplicationRecord
scope :with_video_blobs, -> { includes(:video_blobs).where.not(video_blobs: { id: nil }) }
scope :optimized, -> { includes(:optimized_video_blobs).where.not(video_blobs: { id: nil }) }
scope :optimized_with_checksum, -> { optimized.merge(VideoBlob.checksum) }
+ scope :auto_start, -> { where(auto_start: true) }
+ scope :not_auto_start, -> { where.not(auto_start: true) }
validates :title, presence: true
@@ -51,18 +54,26 @@ def tv?
end
def credits
+ return if the_movie_db_id.nil?
+
@credits ||= "TheMovieDb::#{type}::Credits".constantize.new(the_movie_db_id).results
end
def the_movie_db_details
+ return if the_movie_db_id.nil?
+
@the_movie_db_details ||= "TheMovieDb::#{type}".constantize.new(the_movie_db_id).results
end
def release_dates
+ return if the_movie_db_id.nil?
+
@release_dates ||= "TheMovieDb::#{type}::ReleaseDates".constantize.new(the_movie_db_id).results
end
def ratings
+ return [] if release_dates.nil?
+
@ratings ||= self.class.ratings.keys & release_dates['results']
.flat_map { _1['release_dates'] }
.pluck('certification')
diff --git a/app/queries/video_search_query.rb b/app/queries/video_search_query.rb
index 33cc0c39..273230dc 100644
--- a/app/queries/video_search_query.rb
+++ b/app/queries/video_search_query.rb
@@ -5,7 +5,7 @@ class VideoSearchQuery
VIDEOS_MEDIA_TYPE = %w[movie tv].freeze
RESULTS_PER_PAGE = 21 # the movie db comes back with 20 results per page
- option :query, Types::Coercible::String, optional: true
+ option :query, Types::Coercible::StrippedString, optional: true
option :page, Types::Coercible::Integer, default: -> { 1 }, optional: true, null: nil
def results
diff --git a/app/services/create_disks_service.rb b/app/services/create_disks_service.rb
index 0f599c19..9ed22a1f 100644
--- a/app/services/create_disks_service.rb
+++ b/app/services/create_disks_service.rb
@@ -8,11 +8,7 @@ class CreateDisksService < ApplicationService
def call
return [] if (drives = list_drives).empty?
- disks = drives.map { create_or_update_disks(_1) }
-
- disks.each { _1.update!(ejected: false) }
- broadcast(:disk_loaded)
- disks
+ drives.map { create_or_update_disks(_1) }
end
private
@@ -20,10 +16,11 @@ def call
def create_or_update_disks(drive)
find_or_initalize_disk(drive).tap do |disk|
disk.update!(loading: true)
- broadcast(:disk_loading)
+ broadcast(:disk_loading, disk)
disk.disk_titles.each(&:mark_for_destruction)
find_or_build_disk_titles(disk)
- broadcast(:disk_loaded)
+ disk.update!(ejected: false)
+ broadcast(:disk_loaded, disk)
ensure
disk.update!(loading: false)
end
diff --git a/app/services/movie_disk_title_selector_service.rb b/app/services/movie_disk_title_selector_service.rb
new file mode 100644
index 00000000..b8e05657
--- /dev/null
+++ b/app/services/movie_disk_title_selector_service.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+class MovieDiskTitleSelectorService < ApplicationService
+ Info = Data.define(:disk_title, :extra_type, :within_range, :ripped?, :uploaded?)
+
+ option :disk, Types.Instance(Disk)
+ option :movie, Types.Instance(Movie)
+
+ def call
+ disk_title_sorted.map do |disk_title|
+ Info.new(
+ disk_title,
+ extra_type(disk_title), within_range?(disk_title),
+ ripped?(disk_title), uploaded?(disk_title)
+ )
+ end
+ end
+
+ private
+
+ def extra_type(disk_title)
+ if uploaded?(disk_title)
+ nil
+ elsif within_range?(disk_title) && feature_film_selected?
+ 'shorts'
+ elsif within_range?(disk_title)
+ feature_film_selected!
+ 'feature_films'
+ else
+ 'other'
+ end
+ end
+
+ def ripped?(disk_title)
+ movie.ripped_disk_titles.any? { _1.name == disk_title.name }
+ end
+
+ def uploaded?(disk_title)
+ movie.ripped_disk_titles.find { _1.name == disk_title.name }&.video_blob&.uploaded? || false
+ end
+
+ def within_range?(disk_title)
+ movie.runtime_range.include?(disk_title.duration)
+ end
+
+ def feature_film_selected!
+ @feature_film_selected = true
+ end
+
+ def feature_film_selected?
+ @feature_film_selected == true
+ end
+
+ def disk_title_sorted
+ @disk_title_sorted ||= disk.disk_titles.sort_by do |disk_title|
+ (movie.movie_runtime - disk_title.duration).abs
+ end
+ end
+end
diff --git a/app/views/config/make_mkvs/_form.html.erb b/app/views/config/make_mkvs/_form.html.erb
index a6c729a4..ba8df111 100644
--- a/app/views/config/make_mkvs/_form.html.erb
+++ b/app/views/config/make_mkvs/_form.html.erb
@@ -13,7 +13,7 @@
Install MKV
<% end %>
-
+
<%= f.button :submit %>
diff --git a/app/views/jobs/show.html.erb b/app/views/jobs/show.html.erb
index a7f46dbc..902b55af 100644
--- a/app/views/jobs/show.html.erb
+++ b/app/views/jobs/show.html.erb
@@ -1,4 +1,4 @@
-<% content_for :sidebar_before do %>
+<% content_for :sidebar do %>