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 @@ +
+
<%= title %>
+
<%= body %>
+
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