diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 5c6ebe99..0372f163 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -18,7 +18,7 @@
@import "bootstrap/scss/bootstrap";
@import "minimal";
@import "checkbox_toggle";
-// @import "bg_process";
+@import "bg_process";
// @import "./custom";
// app/javascript/packs/stylesheets/_custom.scss
diff --git a/app/assets/stylesheets/bg_process.scss b/app/assets/stylesheets/bg_process.scss
new file mode 100644
index 00000000..f8de886d
--- /dev/null
+++ b/app/assets/stylesheets/bg_process.scss
@@ -0,0 +1,48 @@
+.bg-process {
+ --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);
+ --bs-toast-border-color: var(--bs-border-color-translucent);
+ --bs-toast-border-radius: var(--bs-border-radius);
+ --bs-toast-border-width: var(--bs-border-width);
+ --bs-toast-box-shadow: var(--bs-box-shadow);
+ --bs-toast-color: ;
+ --bs-toast-font-size: 0.875rem;
+ --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85);
+ --bs-toast-header-border-color: var(--bs-border-color-translucent);
+ --bs-toast-header-color: var(--bs-secondary-color);
+ --bs-toast-max-width: 350px;
+ --bs-toast-padding-x: 0.75rem;
+ --bs-toast-padding-y: 0.5rem;
+ --bs-toast-spacing: 1.5rem;
+ --bs-toast-zindex: 1090;
+ background-clip: padding-box;
+ background-color: $dark-black;
+ border-radius: var(--bs-toast-border-radius);
+ border: var(--bs-toast-border-width) solid var(--bs-toast-border-color);
+ bottom: 0;
+ box-shadow: var(--bs-toast-box-shadow);
+ color: var(--bs-toast-color);
+ font-size: var(--bs-toast-font-size);
+ margin: 2em;
+ max-width: 100%;
+ pointer-events: auto;
+ position: fixed;
+ right: 0;
+ width: var(--bs-toast-max-width);
+ z-index: 4;
+
+ .header {
+ display: flex;
+ align-items: center;
+ padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);
+ color: var(--bs-toast-header-color);
+ background-clip: padding-box;
+ border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);
+ border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));
+ border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));
+ }
+
+ .body {
+ padding: var(--bs-toast-padding-x);
+ word-wrap: break-word;
+ }
+}
diff --git a/app/components/process_component.html.erb b/app/components/process_component.html.erb
new file mode 100644
index 00000000..4d774a4f
--- /dev/null
+++ b/app/components/process_component.html.erb
@@ -0,0 +1,4 @@
+
diff --git a/app/components/process_component.rb b/app/components/process_component.rb
new file mode 100644
index 00000000..53eb7097
--- /dev/null
+++ b/app/components/process_component.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class ProcessComponent < ViewComponent::Base
+ extend Dry::Initializer
+ option :worker, Types.Interface(:name)
+ renders_one :body
+
+ def dom_id
+ "#{worker.name.underscore.dasherize}-process"
+ end
+
+ def title
+ default = worker.name.demodulize.titleize
+ I18n.t("processes.#{worker.name.underscore.dasherize}.title", default:)
+ end
+end
diff --git a/app/controllers/images_controller.rb b/app/controllers/images_controller.rb
index c3702983..f191eac9 100644
--- a/app/controllers/images_controller.rb
+++ b/app/controllers/images_controller.rb
@@ -8,6 +8,7 @@ def show
private
+ # Store the image in a cache directory on localhost to avoid multiple requests
def image
Rails.cache.fetch(image_url, namespace: :images) do
Net::HTTP.get(image_url)
diff --git a/app/controllers/the_movie_dbs_controller.rb b/app/controllers/the_movie_dbs_controller.rb
index b5a9b74f..722245e2 100644
--- a/app/controllers/the_movie_dbs_controller.rb
+++ b/app/controllers/the_movie_dbs_controller.rb
@@ -4,7 +4,9 @@ class TheMovieDbsController < ApplicationController
helper_method :search_service
def index
- ScanPlexWorker.perform_async if Video.none? || Video.maximum(:synced_on) < 20.minutes.ago
+ return unless (Video.none? || !synced_recently?) && !ScanPlexWorker.job.pending?
+
+ ScanPlexWorker.perform_async
end
def next_page
@@ -16,6 +18,10 @@ def next_page
private
+ def synced_recently?
+ Video.maximum(:synced_on) < 5.minutes.ago
+ end
+
def search_service
@search_service ||= VideoSearchQuery.new(**search_params)
end
diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js
index 1213e85c..c84f770d 100644
--- a/app/javascript/controllers/application.js
+++ b/app/javascript/controllers/application.js
@@ -3,7 +3,7 @@ import { Application } from "@hotwired/stimulus"
const application = Application.start()
// Configure Stimulus development experience
-application.debug = false
+application.debug = true
window.Stimulus = application
export { application }
diff --git a/app/models/tv.rb b/app/models/tv.rb
index 3a2b35fb..6875f7df 100644
--- a/app/models/tv.rb
+++ b/app/models/tv.rb
@@ -32,7 +32,7 @@ class Tv < Video
alias_attribute :name, :title
alias_attribute :original_name, :original_title
- serialize :episode_distribution_runtime, Array
+ serialize :episode_distribution_runtime, coder: JSON
with_options unless: :the_movie_db_id do
validates :name, presence: true
diff --git a/app/views/layouts/application.erb b/app/views/layouts/application.erb
index 82ba013b..ef1d24f2 100644
--- a/app/views/layouts/application.erb
+++ b/app/views/layouts/application.erb
@@ -6,6 +6,17 @@
<%= render 'layouts/head' %>
+ <%= render ProcessComponent.new worker: ScanPlexWorker do |c| %>
+ <%= c.with_body do %>
+ <% if ScanPlexWorker.job.pending? %>
+
+ Scanning...
+ <% else %>
+ Done! you have a total of <%= pluralize(Video.count, 'video') %> on plex.
+ <% end %>
+ <% end %>
+ <% end %>
+
<%= render ToastComponent.new do |c| %>
<%= c.body do %>
diff --git a/app/workers/load_disk_worker.rb b/app/workers/load_disk_worker.rb
index 755ea51b..f82f934d 100644
--- a/app/workers/load_disk_worker.rb
+++ b/app/workers/load_disk_worker.rb
@@ -1,15 +1,7 @@
# frozen_string_literal: true
class LoadDiskWorker < ApplicationWorker
- option :disk_id, Types::Integer
-
def perform
CreateDisksService.call
end
-
- private
-
- def disk
- @disk ||= Disk.find(disk_id)
- end
end
diff --git a/app/workers/rip_worker.rb b/app/workers/rip_worker.rb
index 58031522..f446ea34 100644
--- a/app/workers/rip_worker.rb
+++ b/app/workers/rip_worker.rb
@@ -17,14 +17,14 @@ def progress_listener
private
def create_mkv(disk_title)
- CreateMkvService.call disk_title: disk_title,
- progress_listener: progress_listener
+ CreateMkvService.call disk_title:,
+ progress_listener:
end
def upload_mkv(disk_title)
@progress_listener = UploadProgressListener.new(file_size: disk_title.size)
- Ftp::UploadMkvService.call disk_title: disk_title,
- progress_listener: progress_listener
+ Ftp::UploadMkvService.call disk_title:,
+ progress_listener:
end
def disk_titles
diff --git a/app/workers/scan_plex_worker.rb b/app/workers/scan_plex_worker.rb
index b956dc89..3e45a761 100644
--- a/app/workers/scan_plex_worker.rb
+++ b/app/workers/scan_plex_worker.rb
@@ -5,11 +5,45 @@ def perform
plex_movies.map do |blob|
blob.update!(video: create_movie!(blob))
job.log("Updated #{blob.filename}")
+ self.completed += 1
+ broadcast_progress(in_progress_component(blob&.video&.plex_name))
end
+ broadcast_progress(completed_component)
end
private
+ def broadcast_progress(component)
+ cable_ready[DiskTitleChannel.channel_name].morph \
+ selector: "##{component.dom_id}",
+ html: render(component, layout: false)
+ cable_ready.broadcast
+ end
+
+ def completed_component
+ ProcessComponent.new worker: ScanPlexWorker do |c|
+ c.with_body do
+ ProgressBarComponent.new \
+ model: Movie,
+ completed: 100,
+ status: :success,
+ message: 'Plex scan complete!'
+ end
+ end
+ end
+
+ def in_progress_component(message)
+ ProcessComponent.new worker: ScanPlexWorker do |c|
+ c.with_body do
+ ProgressBarComponent.new \
+ model: Movie,
+ completed: (completed / plex_movies.size.to_f * 100),
+ status: :info,
+ message: message || 'Scanning Plex...'
+ end
+ end
+ end
+
def search_for_movie(blob) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
options = { query: blob.parsed_dirname.title, year: blob.parsed_dirname.year }.compact
dirname = TheMovieDb::Search::Movie.new(**options) if options[:query].present?
@@ -31,6 +65,12 @@ def create_movie!(blob)
end
def plex_movies
- Ftp::VideoScannerService.call.movies
+ @plex_movies ||= Ftp::VideoScannerService.call.movies
end
+
+ def completed
+ @completed ||= 0
+ end
+
+ attr_writer :completed
end
diff --git a/config/application.rb b/config/application.rb
index bd33f33b..88b9d655 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -22,7 +22,7 @@
module PlexRipper
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
- config.load_defaults 7.0
+ config.load_defaults 7.1
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 3a1bc73b..8d2e8523 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -20,7 +20,8 @@
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
- config.cache_store = :memory_store
+ # config.cache_store = :memory_store
+ config.cache_store = :file_store, Rails.root.join('tmp/cache')
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
diff --git a/config/environments/production.rb b/config/environments/production.rb
index d718e30d..d7b9d877 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -57,7 +57,7 @@
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
- config.cache_store = :memory_store
+ config.cache_store = :file_store, Rails.root.join('tmp/cache')
# Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
@@ -112,4 +112,6 @@
# config.active_record.database_selector = { delay: 2.seconds }
# config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
# config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ #
+
end
diff --git a/spec/components/process_component_spec.rb b/spec/components/process_component_spec.rb
new file mode 100644
index 00000000..50fa2c4c
--- /dev/null
+++ b/spec/components/process_component_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ProcessComponent, type: :component do
+ pending "add some examples to (or delete) #{__FILE__}"
+
+ # it "renders something useful" do
+ # expect(
+ # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html
+ # ).to include(
+ # "Hello, components!"
+ # )
+ # end
+end