Skip to content

Commit

Permalink
Merge pull request #314 from blackcandy-org/dev
Browse files Browse the repository at this point in the history
Fix #313
  • Loading branch information
aidewoode authored Oct 26, 2023
2 parents 45b3e9f + 79e1538 commit d62d419
Show file tree
Hide file tree
Showing 40 changed files with 252 additions and 186 deletions.
2 changes: 1 addition & 1 deletion app/controllers/albums_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class AlbumsController < ApplicationController
include Playable
include PlayableConcern

before_action :require_admin, only: [:update]
before_action :find_album, except: [:index]
Expand Down
8 changes: 1 addition & 7 deletions app/controllers/api/v1/api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,8 @@ class ApiController < ApplicationController

private

# If user already has logged in then authenticate with current session,
# otherwise authenticate with api token.
def find_current_user
Current.user = UserSession.find&.user

return if logged_in?

authenticate_with_http_token do |token, options|
authenticate_with_http_token do |token, _|
user = User.find_by(api_token: token)
return unless user.present?

Expand Down
40 changes: 1 addition & 39 deletions app/controllers/api/v1/stream_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,7 @@
module Api
module V1
class StreamController < ApiController
before_action :find_stream

def new
send_local_file @stream.file_path
end

private

def set_nginx_header
# Let nginx can get value of media_path dynamically in the nginx config,
# when use X-Accel-Redirect header to send file.
response.headers["X-Media-Path"] = Setting.media_path
response.headers["X-Accel-Redirect"] = File.join("/private_media", @stream.file_path.sub(File.expand_path(Setting.media_path), ""))
end

def find_stream
song = Song.find(params[:song_id])
@stream = Stream.new(song)
end

def send_local_file(file_path)
if BlackCandy::Config.nginx_sendfile?
set_nginx_header

send_file file_path
return
end

# Use Rack::File to support HTTP range without nginx. see https://github.com/rails/rails/issues/32193
Rack::File.new(nil).serving(request, file_path).tap do |(status, headers, body)|
self.status = status
self.response_body = body

headers.each { |name, value| response.headers[name] = value }

response.headers["Content-Type"] = Mime[@stream.format]
response.headers["Content-Disposition"] = "attachment"
end
end
include StreamConcern
end
end
end
45 changes: 2 additions & 43 deletions app/controllers/api/v1/transcoded_stream_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,8 @@

module Api
module V1
class TranscodedStreamController < StreamController
before_action :find_cache

# Similar to send_file in rails, but let response_body to be a stream object.
# The instance of Stream can respond to each() method. So the download can be streamed,
# instead of read whole data into memory.
def new
# Because the module ActionController::Live contains methods that can block send file.
# So can't simply include ActionController::Live in class, otherwise include this module only on this method.
self.class.send(:include, ActionController::Live)

response.headers["Content-Type"] = Mime[Stream::TRANSCODE_FORMAT]

send_stream(filename: "#{@stream.name}.mp3") do |stream_response|
File.open(@stream.transcode_cache_file_path, "w") do |file|
@stream.each do |data|
stream_response.write data
file.write data
end
end
end
end

private

def set_nginx_header
response.headers["X-Accel-Redirect"] = File.join("/private_cache_media", @stream.transcode_cache_file_path.sub(Stream::TRANSCODE_CACHE_DIRECTORY.to_s, ""))
end

def find_cache
return unless valid_cache?
send_local_file @stream.transcode_cache_file_path
end

def valid_cache?
return unless File.exist?(@stream.transcode_cache_file_path)

# Compare duration of cache file and original file to check integrity of cache file.
# Because the different format of the file, the duration will have a little difference,
# so the duration difference in two seconds are considered no problem.
cache_file_tag = WahWah.open(@stream.transcode_cache_file_path)
(@stream.duration - cache_file_tag.duration).abs <= 2
end
class TranscodedStreamController < ApiController
include TranscodedStreamConcern
end
end
end
20 changes: 20 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,24 @@ def turbo_android?
def render_json_error(error, status)
render json: {type: error.type, message: error.message}, status: status
end

def send_local_file(file_path, format, nginx_headers: {})
if BlackCandy::Config.nginx_sendfile?
nginx_headers.each { |name, value| response.headers[name] = value }
send_file file_path

return
end

# Use Rack::File to support HTTP range without nginx. see https://github.com/rails/rails/issues/32193
Rack::File.new(nil).serving(request, file_path).tap do |(status, headers, body)|
self.status = status
self.response_body = body

headers.each { |name, value| response.headers[name] = value }

response.headers["Content-Type"] = Mime[format]
response.headers["Content-Disposition"] = "attachment"
end
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

module Playable
module PlayableConcern
extend ActiveSupport::Concern

included do
Expand Down
24 changes: 24 additions & 0 deletions app/controllers/concerns/stream_concern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module StreamConcern
extend ActiveSupport::Concern

included do
before_action :find_stream
end

def new
send_local_file @stream.file_path, @stream.format, nginx_headers: {
# Let nginx can get value of media_path dynamically in the nginx config
# when use X-Accel-Redirect header to send file.
"X-Media-Path" => Setting.media_path,
"X-Accel-Redirect" => File.join("/private_media", @stream.file_path.sub(File.expand_path(Setting.media_path), ""))
}
end

private

def find_stream
@stream = Stream.new(Song.find(params[:song_id]))
end
end
53 changes: 53 additions & 0 deletions app/controllers/concerns/transcoded_stream_concern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

module TranscodedStreamConcern
extend ActiveSupport::Concern

included do
before_action :find_stream
before_action :find_cache
end

# Similar to send_file in rails, but let response_body to be a stream object.
# The instance of Stream can respond to each() method. So the download can be streamed,
# instead of read whole data into memory.
def new
# Because the module ActionController::Live contains methods that can block send file.
# So can't simply include ActionController::Live in class, otherwise include this module only on this method.
self.class.send(:include, ActionController::Live)

response.headers["Content-Type"] = Mime[Stream::TRANSCODE_FORMAT]

send_stream(filename: "#{@stream.name}.mp3") do |stream_response|
File.open(@stream.transcode_cache_file_path, "w") do |file|
@stream.each do |data|
stream_response.write data
file.write data
end
end
end
end

private

def find_stream
@stream = Stream.new(Song.find(params[:song_id]))
end

def find_cache
return unless valid_cache?
send_local_file @stream.transcode_cache_file_path, @stream.format, nginx_headers: {
"X-Accel-Redirect" => File.join("/private_cache_media", @stream.transcode_cache_file_path.sub(Stream::TRANSCODE_CACHE_DIRECTORY.to_s, ""))
}
end

def valid_cache?
return unless File.exist?(@stream.transcode_cache_file_path)

# Compare duration of cache file and original file to check integrity of cache file.
# Because the different format of the file, the duration will have a little difference,
# so the duration difference in two seconds are considered no problem.
cache_file_tag = WahWah.open(@stream.transcode_cache_file_path)
(@stream.duration - cache_file_tag.duration).abs <= 2
end
end
2 changes: 1 addition & 1 deletion app/controllers/playlists/songs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Playlists::SongsController < ApplicationController
before_action :find_playlist
before_action :find_song, only: [:create, :destroy]

include Playable
include PlayableConcern

def show
@pagy, @songs = pagy(@playlist.songs.includes(:artist))
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/stream_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class StreamController < ApplicationController
include StreamConcern
end
5 changes: 5 additions & 0 deletions app/controllers/transcoded_stream_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class TranscodedStreamController < ApplicationController
include TranscodedStreamConcern
end
7 changes: 5 additions & 2 deletions app/helpers/song_helper.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# frozen_string_literal: true

module SongHelper
def song_json_builder(song)
def song_json_builder(song, for_api: false)
stream_url = for_api ? new_api_v1_stream_url(song_id: song.id) : new_stream_url(song_id: song.id)
transcoded_stream_url = for_api ? new_api_v1_transcoded_stream_url(song_id: song.id) : new_transcoded_stream_url(song_id: song.id)

Jbuilder.new do |json|
json.call(song, :id, :name, :duration)
json.url need_transcode?(song) ? new_api_v1_transcoded_stream_url(song_id: song.id) : new_api_v1_stream_url(song_id: song.id)
json.url need_transcode?(song) ? transcoded_stream_url : stream_url
json.album_name song.album.title
json.artist_name song.artist.title
json.is_favorited song.is_favorited.nil? ? Current.user.favorited?(song) : song.is_favorited
Expand Down
8 changes: 4 additions & 4 deletions app/models/album.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# frozen_string_literal: true

class Album < ApplicationRecord
include Searchable
include Imageable
include Filterable
include Sortable
include SearchableConcern
include ImageableConcern
include FilterableConcern
include SortableConcern

validates :name, uniqueness: {scope: :artist}

Expand Down
6 changes: 3 additions & 3 deletions app/models/artist.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# frozen_string_literal: true

class Artist < ApplicationRecord
include Searchable
include Imageable
include Sortable
include SearchableConcern
include ImageableConcern
include SortableConcern

has_many :albums, dependent: :destroy
has_many :songs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

module Filterable
module FilterableConcern
extend ActiveSupport::Concern

included do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

module GlobalSetting
module GlobalSettingConcern
extend ActiveSupport::Concern

included do
Expand All @@ -22,12 +22,12 @@ def has_setting(setting, type: :string, default: nil)
store :values, accessors: setting

define_method("#{setting}=") do |value|
setting_value = ScopedSetting.convert_setting_value(type, value)
setting_value = ScopedSettingConcern.convert_setting_value(type, value)
super(setting_value)
end

define_method(setting) do
ScopedSetting.convert_setting_value(type, super())
ScopedSettingConcern.convert_setting_value(type, super())
end

define_singleton_method(setting) do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Imageable
module ImageableConcern
extend ActiveSupport::Concern

included do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

module ScopedSetting
module ScopedSettingConcern
extend ActiveSupport::Concern

included do
Expand All @@ -14,7 +14,7 @@ def has_setting(setting, type: :string, default: nil)
store :settings, accessors: setting

define_method(setting) do
setting_value = ScopedSetting.convert_setting_value(type, super())
setting_value = ScopedSettingConcern.convert_setting_value(type, super())

setting_value || default
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

module Searchable
module SearchableConcern
extend ActiveSupport::Concern

class_methods do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

module Sortable
module SortableConcern
extend ActiveSupport::Concern

included do
Expand Down
4 changes: 2 additions & 2 deletions app/models/playlist.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# frozen_string_literal: true

class Playlist < ApplicationRecord
include Searchable
include Sortable
include SearchableConcern
include SortableConcern

validates :name, presence: true, if: :require_name?

Expand Down
2 changes: 1 addition & 1 deletion app/models/setting.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class Setting < ApplicationRecord
include GlobalSetting
include GlobalSettingConcern

AVAILABLE_BITRATE_OPTIONS = [128, 192, 320].freeze

Expand Down
Loading

0 comments on commit d62d419

Please sign in to comment.