Skip to content

Commit

Permalink
Replace Nginx with Thruster to send file
Browse files Browse the repository at this point in the history
  • Loading branch information
aidewoode committed Nov 14, 2024
1 parent 1812af7 commit ad36299
Show file tree
Hide file tree
Showing 13 changed files with 199 additions and 71 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ gem "daemons", "~> 1.4.0"
# Optional support for postgresql as database
gem "pg", "~> 1.5.4"

# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
gem "thruster", "~> 0.1.8", require: false

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", "~> 1.18.0", require: false

Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ GEM
railties (>= 6.0.0)
stringio (3.1.1)
thor (1.3.2)
thruster (0.1.8)
timeout (0.4.1)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
Expand Down Expand Up @@ -428,6 +429,7 @@ DEPENDENCIES
standard (~> 1.25.0)
standard-rails
stimulus-rails (~> 1.3.4)
thruster (~> 0.1.8)
turbo-rails (~> 1.5.0)
tzinfo-data
wahwah (~> 1.6.0)
Expand Down
20 changes: 0 additions & 20 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,26 +126,6 @@ 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::Files to support HTTP range without nginx. see https://github.com/rails/rails/issues/32193
Rack::Files.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

def find_current_request_details
Current.ip_address = request.ip
Current.user_agent = request.user_agent
Expand Down
7 changes: 1 addition & 6 deletions app/controllers/concerns/stream_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ module StreamConcern
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), ""))
}
send_file @stream.file_path
end

private
Expand Down
4 changes: 1 addition & 3 deletions app/controllers/concerns/transcoded_stream_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ def find_stream

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, ""))
}
send_file @stream.transcode_cache_file_path
end

def valid_cache?
Expand Down
5 changes: 5 additions & 0 deletions bin/thrust
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"

load Gem.bin_path("thruster", "thrust")
1 change: 0 additions & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ module BlackCandy
has_config :queue_db_url
has_config :media_path
has_config :db_adapter, default: "sqlite"
has_config :nginx_sendfile, default: false
has_config :force_ssl, default: false
has_config :demo_mode, default: false

Expand Down
9 changes: 0 additions & 9 deletions config/nginx/sendfile.conf

This file was deleted.

2 changes: 1 addition & 1 deletion docker/production_start.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh

rails db:prepare
bundle exec puma -C config/puma.rb
./bin/thrust ./bin/rails server
186 changes: 186 additions & 0 deletions docs/README_EDGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<p align='center'>
<img alt='Black Candy logo' width='200' src='https://raw.githubusercontent.com/blackcandy-org/black_candy/master/app/assets/images/logo.svg'>
</p>

# Black Candy
[![CI](https://github.com/blackcandy-org/black_candy/actions/workflows/ci.yml/badge.svg)](https://github.com/blackcandy-org/black_candy/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/github/blackcandy-org/blackcandy/badge.svg?branch=master)](https://coveralls.io/github/blackcandy-org/black_candy?branch=master)
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
![Docker Pulls](https://img.shields.io/docker/pulls/blackcandy/blackcandy)

![Screenshot](https://raw.githubusercontent.com/blackcandy-org/blackcandy/master/docs/images/screenshot_main.png)

Black Candy is a self-hosted music streaming server, your personal music center.

## Try The Demo

Please visit <https://demo.blackcandy.org> and use demo user (email: admin@admin.com, password: foobar) to log in. And feel free to try it.

> [!NOTE]
> This demo user does not have administrator privileges. So you cannot experience all the features in Black Candy. And all music in the demo are from [Free Music Archive](https://freemusicarchive.org/). You can check their [licenses](https://github.com/blackcandy-org/blackcandy/blob/master/docs/demo_music_licenses.md).
## Installation

Black Candy uses docker image to install easily. You can run Black Candy like this.

```shell
docker run -p 3000:3000 ghcr.io/blackcandy-org/blackcandy:latest

# Or pull from Docker Hub.
docker run -p 3000:3000 blackcandy/blackcandy:latest
```

That's all. Now, you can access either http://localhost:3000 or http://host-ip:3000 in a browser, and use initial admin user to log in (email: admin@admin.com, password: foobar).

## Upgrade

> [!IMPORTANT]
> If you upgrade to a major version, you need to read the upgrade guide carefully before upgrade. Because there are some breaking changes in a major version.
>
> - See [V3 Upgrade](https://github.com/blackcandy-org/blackcandy/blob/master/docs/v3_upgrade.md) for upgrade from V2 release.
> - See [Edge Upgrade](https://github.com/blackcandy-org/blackcandy/blob/master/docs/edge_upgrade.md) for upgrade from edge release to latest stable release.
Upgrade Black Candy is pull new image from remote. Then remove an old container and create a new one.

```shell
docker pull ghcr.io/blackcandy-org/blackcandy:latest
docker stop <your_blackcandy_container>
docker rm <your_blackcandy_container>
docker run <OPTIONS> ghcr.io/blackcandy-org/blackcandy:latest
```

With docker compose, you can upgrade Black Candy like this:

```shell
docker pull ghcr.io/blackcandy-org/blackcandy:latest
docker-compose down
docker-compose up
```

## Mobile Apps

Black Candy mobile apps are available in the following app stores:

[<img src="https://raw.githubusercontent.com/blackcandy-org/ios/master/images/appstore_badge.png" alt="Get it on App Store" height="50">](https://apps.apple.com/app/blackcandy/id6444304071)
[<img src="https://raw.githubusercontent.com/blackcandy-org/android/master/images/fdroid_badge.png" alt="Get it on F-Droid" height="50">](https://f-droid.org/packages/org.blackcandy.android/)


For Android app, you can also download APK from [GitHub Release](https://github.com/blackcandy-org/android/releases/latest)

## Configuration

### Port Mapping

Black Candy exports the 3000 port. If you want to be able to access it from the host, you can use the `-p` option to map the port.

```shell
docker run -p 3000:3000 ghcr.io/blackcandy-org/blackcandy:latest
```

### Media Files Mounts

You can mount media files from host to container and use `MEDIA_PATH` environment variable to set the media path for black candy.

```shell
docker run -v /media_data:/media_data -e MEDIA_PATH=/media_data ghcr.io/blackcandy-org/blackcandy:latest
```

### Use PostgreSQL As Database

Black Candy use SQLite as database by default. Because SQLite can simplify the process of installation, and it's an ideal choice for self-hosted small server. If you think SQLite is not enough, or you are using some cloud service like heroku to host Black Candy, you can also use PostgreSQL as database.

```shell
docker run -e DB_ADAPTER=postgresql -e DB_URL=postgresql://yourdatabaseurl ghcr.io/blackcandy-org/blackcandy:latest
```

### How to Persist Data

All the data that need to persist in Black Candy are stored in `/app/storage`, So you can mount this directory to host to persist data.

```shell
mkdir storage_data

docker run -v ./storage_data:/app/storage ghcr.io/blackcandy-org/blackcandy:latest
```

### 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/.

## Environment Variables

| Name | Default | Description |
| --- | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DB_URL | | The URL of PostgreSQL database. You must set this environment variable if you use PostgreSQL as database. |
| CABLE_DB_URL | | The URL of Pub/Sub database. You must set this environment variable if you use PostgreSQL as database. |
| QUEUE_DB_URL | | The URL of background job database. You must set this environment variable if you use PostgreSQL as database. |
| CACHE_DB_URL | | The URL of cache database. You must set this environment variable if you use PostgreSQL as database. |
| MEDIA_PATH | | You can use this environment variable to set media path for Black Candy, otherwise you can set media path in settings page. |
| DB_ADAPTER | "sqlite" | There are two adapters are supported, "sqlite" and "postgresql". |
| SECRET_KEY_BASE | | When the SECRET_KEY_BASE environment variable is not set, Black candy will generate SECRET_KEY_BASE environment variable every time when service start up. This will cause old sessions invalid, You can set your own SECRET_KEY_BASE environment variable on docker service to avoid it. |
| FORCE_SSL | false | Force all access to the app over SSL. |
| DEMO_MODE | false | Whether to enable demo mode, when demo mode is on, all users cannot access administrator privileges, even user is admin. And also users cannot change their profile. |

## Edge Version

The edge version of Black Candy base on master branch, which means it's not stable, you may encounter data loss or other issues. However, I don't recommend normal user using an edge version. But if you are a developer who wants to test or contribute to Black Candy, you can use the edge version.

```shell
docker pull ghcr.io/blackcandy-org/blackcandy:edge
```

## Development

### Requirements

- Ruby 3.3
- Node.js 20
- libvips
- FFmpeg

Make sure you have installed all those dependencies.

### Install gem dependencies

```shell
bundle install
```

### Install JavaScript dependencies

```shell
npm install
```

### Database Configuration

```shell
rails db:prepare
rails db:seed
```

### Start all services

After you’ve set up everything, now you can run `./bin/dev` to start all services you need to develop.
Then visit <http://localhost:3000> use initial admin user to log in (email: admin@admin.com, password: foobar).

### Running tests

```shell
# Running all test
$ rails test:all

# Running lint
$ rails lint:all
```

## Integrations

Black Candy support get artist and album image from Discogs API. You can create an API token from Discogs and set Discogs token on Setting page to enable it.

## Sponsorship

This project is supported by:

<a href="https://www.digitalocean.com/"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="200px"></a>
<a href="https://www.jetbrains.com/community/opensource"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_square.svg"></a>
9 changes: 0 additions & 9 deletions test/controllers/api/v1/stream_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,6 @@ class Api::V1::StreamControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end

test "should set header for nginx send file" do
with_env("NGINX_SENDFILE" => "true") do
get new_api_v1_stream_url(song_id: songs(:mp3_sample).id), headers: api_token_header(@user)

assert_equal Setting.media_path, @response.get_header("X-Media-Path")
assert_equal "/private_media/artist1_album2.mp3", @response.get_header("X-Accel-Redirect")
end
end

test "should respond file data" do
get new_api_v1_stream_url(song_id: songs(:mp3_sample).id), headers: api_token_header(@user)
assert_equal binary_data(file_fixture("artist1_album2.mp3")), response.body
Expand Down
13 changes: 0 additions & 13 deletions test/controllers/api/v1/transcoded_stream_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,6 @@ def close_tmp_cache_file
end
end

test "should send cached transcoded stream file when found cache and send file with nginx" do
Stream.stub(:new, @stream_mock) do
get new_api_v1_transcoded_stream_url(song_id: songs(:flac_sample).id), headers: api_token_header(@user)
assert_response :success

with_env("NGINX_SENDFILE" => "true") do
get new_api_v1_transcoded_stream_url(song_id: songs(:flac_sample).id), headers: api_token_header(@user)
assert_equal "/private_cache_media#{@stream_mock.transcode_cache_file_path}", @response.get_header("X-Accel-Redirect")
assert_equal "audio/mpeg", @response.get_header("Content-Type")
end
end
end

test "should regenerate new cache when cache is invalid" do
Stream.stub(:new, @stream_mock) do
stream = Stream.new(songs(:flac_sample))
Expand Down
9 changes: 0 additions & 9 deletions test/lib/black_candy/config_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,6 @@ class BlackCandy::ConfigTest < ActiveSupport::TestCase
end
end

test "should get nginx_sendfile value as a boolean" do
assert_nil ENV["NGINX_SENDFILE"]
assert_not BlackCandy.config.nginx_sendfile?

with_env("NGINX_SENDFILE" => "true") do
assert BlackCandy.config.nginx_sendfile?
end
end

test "should raise error when database_adapter is not supported" do
with_env("DB_ADAPTER" => "invalid_adapter") do
assert_raises(BlackCandy::Config::ValidationError) do
Expand Down

0 comments on commit ad36299

Please sign in to comment.