Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #313 #314

Merged
merged 3 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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