Skip to content

Commit

Permalink
Merge 314f881 into 2fea21f
Browse files Browse the repository at this point in the history
  • Loading branch information
aidewoode committed Dec 21, 2023
2 parents 2fea21f + 314f881 commit fe9383f
Show file tree
Hide file tree
Showing 48 changed files with 419 additions and 130 deletions.
5 changes: 4 additions & 1 deletion Dockerfile
Expand Up @@ -5,7 +5,7 @@ FROM base AS builder
ENV RAILS_ENV production
ENV NODE_ENV production

# build for musl-libc, not glibc (see https://github.com/sparklemotion/nokogiri/issues/2075, https://github.com/rubygems/rubygems/issues/3174)
# Build for musl-libc, not glibc (see https://github.com/sparklemotion/nokogiri/issues/2075, https://github.com/rubygems/rubygems/issues/3174)
ENV BUNDLE_FORCE_RUBY_PLATFORM 1

COPY --from=node /usr/local/bin/node /usr/local/bin/node
Expand Down Expand Up @@ -56,6 +56,9 @@ RUN addgroup -g 1000 -S app && adduser -u 1000 -S app -G app
COPY --from=builder --chown=app:app /usr/local/bundle/ /usr/local/bundle/
COPY --from=builder --chown=app:app /app/ /app/

# Forwards media listener logs to stdout so they can be captured in docker logs.
RUN ln -sf /dev/stdout /app/log/media_listener_production.log

USER app

ENTRYPOINT ["docker/entrypoint.sh"]
Expand Down
5 changes: 4 additions & 1 deletion Gemfile
Expand Up @@ -20,7 +20,7 @@ gem "cssbundling-rails", "~> 1.2.0"
gem "jsbundling-rails", "~> 1.1.2"

# Use Puma as the app server
gem "puma", "~> 6.3.0"
gem "puma", "~> 6.4.0"

# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem "jbuilder", "~> 2.11.5"
Expand Down Expand Up @@ -52,6 +52,9 @@ gem "bcrypt", "~> 3.1.11"
# For sync on library changes
gem "listen", "~> 3.8.0"

# For daemonize library sync process
gem "daemons", "~> 1.4.0"

# Default stack for database, cache, background job and pub/sub
gem "litestack", "~> 0.4.2"

Expand Down
10 changes: 6 additions & 4 deletions Gemfile.lock
Expand Up @@ -124,6 +124,7 @@ GEM
cuprite (0.14.3)
capybara (~> 3.0)
ferrum (~> 0.13.0)
daemons (1.4.1)
date (3.3.3)
debug (1.8.0)
irb (>= 1.5.0)
Expand Down Expand Up @@ -231,8 +232,8 @@ GEM
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.2)
bigdecimal (~> 3.1)
oj (3.16.3)
bigdecimal (>= 3.0)
pagy (6.0.4)
parallel (1.23.0)
parser (3.2.2.4)
Expand All @@ -247,7 +248,7 @@ GEM
psych (5.1.1.1)
stringio
public_suffix (5.0.3)
puma (6.3.1)
puma (6.4.0)
nio4r (~> 2.0)
racc (1.7.1)
rack (3.0.8)
Expand Down Expand Up @@ -401,6 +402,7 @@ DEPENDENCIES
carrierwave (~> 3.0.0)
cssbundling-rails (~> 1.2.0)
cuprite (~> 0.14.3)
daemons (~> 1.4.0)
debug
erb_lint (~> 0.4.0)
httparty (~> 0.21.0)
Expand All @@ -413,7 +415,7 @@ DEPENDENCIES
pagy (~> 6.0.0)
pg (~> 1.3.2)
propshaft (~> 0.7.0)
puma (~> 6.3.0)
puma (~> 6.4.0)
rails (~> 7.1.0)
ransack (~> 4.1.0)
redis (~> 4.0)
Expand Down
18 changes: 0 additions & 18 deletions README.md
Expand Up @@ -143,24 +143,6 @@ services:
But you can also use embedded mode of Sidekiq if you don't want another separate Sidekiq process. This can help your deployment become easier.
All you need to do is to set `EMBEDDED_SIDEKIQ` environment variable to true.

### Listener For Media Library

Listener for media library can automatically sync for media library changes. You need another process to run the listener.

```yaml
version: '3'
services:
app: &app_base
image: ghcr.io/blackcandy-org/blackcandy:edge
volumes:
- ./storage_data:/app/storage
- ./uploads_data:/app/public/uploads
- /media_data:/media_data
listener:
<<: *app_base
command: bundle exec rails listen_media_changes
```

### Logging

Black Candy logs to `STDOUT` by default. So if you want to control the log, Docker already supports a lot of options to handle the log in the container. see: https://docs.docker.com/config/containers/logging/configure/.
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/components/_button.scss
Expand Up @@ -8,6 +8,7 @@
padding: spacing("tiny") spacing("narrow");
color: var(--btn-color);
cursor: pointer;
font-size: font-size("medium");
}

.c-button--primary {
Expand Down
2 changes: 2 additions & 0 deletions app/assets/stylesheets/settings/_theme.scss
Expand Up @@ -30,6 +30,7 @@ $green: rgb(35, 166, 80);
/* Text */
--text-primary-color: #{$grey-900};
--text-secondary-color: #{$grey-600};
--text-success-color: #{$green};

/* Sidebar */
--sidebar-bg-color: #{$grey-100};
Expand Down Expand Up @@ -136,6 +137,7 @@ $green: rgb(35, 166, 80);
/* Text */
--text-primary-color: #{$white};
--text-secondary-color: #{$grey-300};
--text-success-color: #{$green};

/* Sidebar */
--sidebar-bg-color: #{$grey-900};
Expand Down
4 changes: 4 additions & 0 deletions app/assets/stylesheets/utilities/_text.scss
Expand Up @@ -22,6 +22,10 @@
color: var(--text-secondary-color) !important;
}

.u-text-color-success {
color: var(--text-success-color) !important;
}

.u-text-monospace {
font-family: variables.$monospace-fonts;
}
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Expand Up @@ -48,7 +48,7 @@ def need_transcode?(song)
return true if browser.safari? && !song_format.in?(Stream::SAFARI_SUPPORTED_FORMATS)
return true if ios_app? && !song_format.in?(Stream::IOS_SUPPORTED_FORMATS)

Setting.allow_transcode_lossless ? song.lossless? : false
Setting.allow_transcode_lossless? ? song.lossless? : false
end

def flash_errors_message(object, now: false)
Expand Down
14 changes: 14 additions & 0 deletions app/controllers/media_syncing_controller.rb
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class MediaSyncingController < ApplicationController
before_action :require_admin

def create
if Media.syncing?
flash[:error] = t("error.syncing_in_progress")
redirect_to setting_path
else
MediaSyncJob.perform_later
end
end
end
1 change: 0 additions & 1 deletion app/controllers/settings_controller.rb
Expand Up @@ -10,7 +10,6 @@ def update
setting = Setting.instance

if setting.update(setting_params)
MediaSyncJob.perform_later if setting_params[:media_path]
flash.now[:success] = t("success.update")
else
flash_errors_message(setting, now: true)
Expand Down
6 changes: 5 additions & 1 deletion app/jobs/media_sync_job.rb
Expand Up @@ -2,6 +2,10 @@ class MediaSyncJob < ApplicationJob
queue_as :default

def perform(type = :all, file_paths = [])
Media.sync(type, file_paths)
if type == :all
Media.sync_all
else
Media.sync(type, file_paths)
end
end
end
5 changes: 5 additions & 0 deletions app/models/concerns/global_setting_concern.rb
Expand Up @@ -36,6 +36,11 @@ def has_setting(setting, type: :string, default: nil)

setting_value || default_value
end

if type == :boolean
alias_method "#{setting}?", setting
singleton_class.send(:alias_method, "#{setting}?", setting)
end
end
end
end
18 changes: 10 additions & 8 deletions app/models/media.rb
Expand Up @@ -3,15 +3,17 @@
class Media
include Singleton
include Turbo::Broadcastable

extend ActiveModel::Naming

@@syncing = false

class << self
def sync(type = :all, file_paths = [])
def sync_all(dir = Setting.media_path)
sync(:all, MediaFile.file_paths(dir))
ensure
instance.broadcast_render_to "media_sync", partial: "media_syncing/syncing", locals: {syncing: false}
end

def sync(type, file_paths = [])
self.syncing = true
file_paths = MediaFile.file_paths if type == :all

return if file_paths.blank?

Expand All @@ -32,12 +34,12 @@ def sync(type = :all, file_paths = [])
end

def syncing?
@@syncing
Rails.cache.fetch("media_syncing") { false }
end

def syncing=(is_syncing)
@@syncing = is_syncing
instance.broadcast_render_to "media_sync", partial: "settings/media_sync"
return if is_syncing == syncing?
Rails.cache.write("media_syncing", is_syncing, expires_in: 1.hour)
end

private
Expand Down
8 changes: 5 additions & 3 deletions app/models/media_file.rb
Expand Up @@ -4,8 +4,10 @@ class MediaFile
SUPPORTED_FORMATS = WahWah.support_formats.freeze

class << self
def file_paths
media_path = File.expand_path(Setting.media_path)
def file_paths(media_path)
return [] if media_path.blank?

absolute_media_path = File.expand_path(media_path)

# Because Ruby ignores the FNM_CASEFOLD flag in Dir.glob, case sensitivity depends on your system.
# So we need another way to make Dir.glob case-insensitive.
Expand All @@ -14,7 +16,7 @@ def file_paths
format.chars.map { |char| (char.downcase != char.upcase) ? "[#{char.downcase}#{char.upcase}]" : char }.join
end

Dir.glob("#{media_path}/**/*.{#{case_insensitive_supported_formats.join(",")}}")
Dir.glob("#{absolute_media_path}/**/*.{#{case_insensitive_supported_formats.join(",")}}")
end

def format(file_path)
Expand Down
57 changes: 57 additions & 0 deletions app/models/media_listener.rb
@@ -0,0 +1,57 @@
# frozen_string_literal: true

class MediaListener
include Singleton

SERVICE_PATH = File.join(Rails.root, "lib", "daemons", "media_listener_service")
class Config
DEFAULTS = {
service_name: "media_listener_service",
pid_dir: File.join(Rails.root, "tmp", "pids")
}

attr_accessor :service_name, :pid_dir

def initialize
DEFAULTS.each do |key, value|
send("#{key}=", value)
end
end
end

class << self
delegate :start, :stop, :running?, to: :instance

def config
yield instance.config
end
end

attr_reader :config

def initialize
@config = Config.new
end

def start
run("start")
end

def stop
run("stop")
end

def running?
pid_file_path = Daemons::PidFile.find_files(@config.pid_dir, @config.service_name).first
return false unless pid_file_path.present?

pid_file = Daemons::PidFile.existing(pid_file_path)
Daemons::Pid.running?(pid_file.pid)
end

private

def run(command)
system "bundle exec #{SERVICE_PATH} #{command} -d #{@config.pid_dir} -n #{@config.service_name}"
end
end
18 changes: 17 additions & 1 deletion app/models/setting.rb
Expand Up @@ -9,18 +9,34 @@ class Setting < ApplicationRecord
has_setting :discogs_token
has_setting :transcode_bitrate, type: :integer, default: 128
has_setting :allow_transcode_lossless, type: :boolean, default: false
has_setting :enable_media_listener, type: :boolean, default: false

validates :transcode_bitrate, inclusion: {in: AVAILABLE_BITRATE_OPTIONS}, allow_nil: true
validate :media_path_exist

after_update :toggle_media_listener, if: :saved_change_to_enable_media_listener?
after_update_commit :sync_media, if: :saved_change_to_media_path?

private

def media_path_exist
return if media_path.blank?
errors.add(:media_path, :blank) and return if media_path.blank?

path = File.expand_path(media_path)

errors.add(:media_path, :not_exist) unless File.exist?(path)
errors.add(:media_path, :unreadable) unless File.readable?(path)
end

def sync_media
MediaSyncJob.perform_later
end

def toggle_media_listener
if enable_media_listener?
MediaListener.start
else
MediaListener.stop
end
end
end
21 changes: 21 additions & 0 deletions app/views/media_syncing/_button.html.erb
@@ -0,0 +1,21 @@
<div id="turbo-media-syncing-button">
<% if syncing %>
<%= button_to(
t("label.syncing"),
false,
class: "c-button c-button--secondary",
data: {test_id: "media_sync_button"},
disabled: true
) %>
<% else %>
<%= button_to(
t("label.sync"),
media_syncing_path,
class: "c-button c-button--secondary",
data: {
test_id: "media_sync_button",
turbo_submits_with: t("label.syncing")
}
) %>
<% end %>
</div>
5 changes: 5 additions & 0 deletions app/views/media_syncing/_syncing.turbo_stream.erb
@@ -0,0 +1,5 @@
<%= turbo_stream.replace "turbo-media-syncing-button", partial: "media_syncing/button", locals: {syncing: syncing} %>
<% unless syncing %>
<%= render_flash(message: t("text.sync_completed")) %>
<% end %>
1 change: 1 addition & 0 deletions app/views/media_syncing/create.turbo_stream.erb
@@ -0,0 +1 @@
<%= render partial: "media_syncing/syncing", locals: { syncing: true } %>

0 comments on commit fe9383f

Please sign in to comment.