To run the server you need to have Bun installed.
- Clone the project and enter the directory.
- Install the dependencies by running
bun install
. - Check the configuration.
- Run
bun run build
to build the website.
You can then start the server with bun start
or you can use bun cluster
to start multiple servers as a cluster.
Make sure to read the configuration setup first.
- Copy
docker-compose.example.yaml
todocker-compose.yaml
and adjust the port and mount points to your setup. - Run
docker compose up
to build and start the server.
Use docker compose exec app bun cli
to interact with the CLI.
The configuration will be read from ./config.toml
.
[site]
site_name = 'Faccina'
url = 'https://example.com'
enable_users = true
admin_users = ['superuser']
default_sort = 'released_at'
default_order = 'desc'
guest_downloads = true
search_placeholder = ''
store_og_images = true
secure_session_cookie = true
client_side_downloads = true
site_name
: Specifies the title showed in pages and emails.url
: Public URL of the site.enable_users
: Used to enable/disable user features such as user registration, login and favorites. Use theuli
command to login as admin.enable_collections
: Enable user collections.enable_analytics
: Enable site analytics.admin_users
: List of usernames that will be given admin privileges. If you use theuli
command to login as an admin user and this user does not exists, a new one will be created.default_sort
: Default sorting when nothing was specified by the user.default_order
: Default ordering when nothing was specified by the user.guest_downloads
: Show download button for guests users.search_placeholder
: Placeholder text for the search bar.store_og_images
: Save generated OpenGraph meta images on disk.secure_session_cookie
: Indicate if the session cookie should be secure or not.client_side_downloads
: Indicate if it gallery downloads should happen on the client or not. Client-side downloads work by downloading the image in the user's browser and packaging the ZIP locally.
page_limits = [24]
default_page_limit = 24
page_limits
: Array of numbers containing the options for the "Per page" filter in gallery listings.default_page_limit
: The default option in the "Per page" filter. If this is not specified, the first page limit will be used.
You can create a tag weight map to assign weights to namespace:tag
combinations. These are used for gallery listings. Useful to showcase important tags before opening a gallery.
[[site.gallery_listing.tag_weight]]
name = ['illustration', 'cg-set', 'cg set']
weight = 20
ignore_case = true
[[site.gallery_listing.tag_weight]]
name = 'color'
weight = 15
ignore_case = true
By default, all tags have 0 weight, so these tags will be displayed first in listings.
You can exclude tags from displaying in the gallery card on listings.
[[site.gallery_listing.tag_exclude]]
name = ['original', 'original work']
ignore_case = true
This will exclude original
and original work
tags in all namespaces from displaying in the gallery card.
[directories]
content = '/path/to/archives'
images = '/path/to/images'
content
: Where CBZ archives are located. Use a read-only mount for safety.images
: Where generated images will be located.
The server supports both PostgreSQL and SQLite.
SQLite is blazingly fast but can cause problems when multiple writers are needed.
This might present problems when users are involved in the equation. Registration, login and favorites all need to write to the database. Add to that indexing and image dimension calculation.
[database]
vendor = 'sqlite'
path = '/path/to/db.sqlite3'
apply_optimizations = true # default
apply_optimizations
will run these queries when initializing a SQLite connection:
PRAGMA journal_mode = wal;
PRAGMA synchronous = normal;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = true;
[database]
vendor = 'postgresql'
user = 'db_admin'
database = 'faccina'
password = 'supersecurepassword'
host = '127.0.0.1'
port = 5432
If using PostresSQL 15 and newer (https://www.postgresql.org/about/news/postgresql-15-released-2526/) you need to do this to the created database:
ALTER DATABASE faccina OWNER TO db_admin;
Metadata parsing options.
[metadata]
parse_filename_as_title = true # default
parse_filename_as_title
: If the available metadata didn't offer a title, indicate if it should try to get a title from the filename or use the filename as the gallery title.
Example: "[Artist] My Gallery Title" will become "My Gallery Title".
You can create a tag map to assign a namespace:tag
combination to another new_namespace:new_tag
combo.
[[metadata.tag_mapping]]
match = 'original'
match_namespace = 'parody'
name = 'original work'
ignore_case = true
For this example, any archive containing the parody:original
tag will be mapped to parody:original work
The match_namespace
key is an optional used to only restrict tags on the parody
namespace. With this example, tag:original
will not get mapped.
To indicate if casing should be ignored for the match specify the ignore_case
key: ignore_case = true
. Using the example config, parody:Original
will be mapped to parody:original work
.
You can also choose to which namespace map the tag by specifing the namespace
key.
[[metadata.tag_mapping]]
match = ['fate grand order', 'Fate/Grand Order']
namespace = 'parody'
This will make the tag tag:fate grand order
and tag:Fate/Grand Order
to parody:fate grand order
and parody:Fate/Grand Order
respectively.
Similar to tag mapping, you can map source names so they can be asigned to URLs when no name is given by the archive metadata.
[[metadata.source_mapping]]
match = 'pixiv'
name = 'Pixiv'
ignore_case = true
For this example, if the archive metadata only provides a URL like this: https://www.pixiv.net/en/artworks/12345678/
then the name Pixiv
will be added to it. By default, if no source name was providad by the archive metadata and mappins, then it will default to the hostname in the URL given that the URL is valid, if not, the source will not be added during indexing.
If the given source already has a name paired to it, the name will be replace if a match is found.
Default configuration
[image]
cover_preset = 'cover'
thumbnail_preset = 'cover'
aspect_ratio_similar = true
remove_on_update = true
[image.preset.cover]
format = 'webp'
width = 540
[image.preset.thumbnail]
format = 'webp'
width = 360
cover_preset
: Indicates which preset to use for covers.thumbnail_preset
: Indicates which preset to use for thumbnails.aspect_ratio_similar
: If enabled, images that are similar to 45:64 (0.703125) aspect ratio will be adapted to it. This will trigger if the aspect ratio is between 0.65 and 0.75. Example, 2:3 (0.66) aspect ratio will be transformed to 45:64.remove_on_update
: If enabled during indexing, any image change will remove the resampled image. Useful when changing filename:page number pairs.
By default, the server will send a Cache-Control header in the image response:
Cache-Control: public, max-age=432000, immutable
.
You can configure the seconds for every type of image (page, thumbnail or cover) with:
[image.caching]
page = 31104000 # 365 days
thumbnail = 172800 # 2 days
cover = 432000 # 5 days
You can also specify seconds for all the types with:
[image]
caching = 432000
To disable setting a Cache-Control header use:
[image]
caching = false
Below are the default encoding options for every supported format that can be overriden.
# WEBP default configuration
quality = 80 # 1-100
lossless = false
speed = 4 # 0-6
# JPEG default configuration
quality = 75 # 1-100
# JXL default configuration
quality = 1 # 0.0-15.0
lossless = false
speed = 7 # 1-10
# AVIF default configuration
quality = 80 # 1-100
speed = 4 # 1-10
You can use these as a reference to override encoding options in a specific preset or globally for all presets.
A image encdoing preset can be made to be used for encoding covers, thumbnails or resampled images.
Example:
[image.preset.jxl480]
format = 'jxl' # webp, jpeg, png, jxl, avif
width = 480
quality = 70
speed = 8
To use this preset for covers:
[image]
cover_preset = 'jxl480'
On a cover request, the server will look for a file named images/{hash}/jxl480/{page_number}.jxl
. If it doesn't exists, it will be added to a queue to be generated.
Use thumbnail_preset
for page thumbnails.
The server can send emails for account access recovery. If no options are specified, account access recovery will be disabled.
[mailer]
host = "smtp.example.com"
port = 465
secure = true
user = "smtp_username"
pass = "smtp_password"
from = "admin@example.com"
To operate with the site you need to use the CLI. You can run bun cli --help
to see the available commands and a quick summary.
To index archives located in the content directory, run bun cli index
.
You can pass the -f
or --force
option to update indexed archives. Use this if you want to update the metadata or contents of your archives.
Use the --reindex
option to only update alredy indexed archives and skip new ones.
To only index certain paths, you can use -p <paths...>
or --paths <paths...>
and indicate a space separated list of paths to index. This is useful if you want to index something that isn't in the content directory or to update a specific archive.
If you want to resume indexing, you can use the --from-path <path>
option to indicate which path (in the content directory) should start indexing from.
Note: some paths are incompatible with NPM scripts so if you use --from-path <path>
, -p <paths...>
or --paths <paths...>
and any of these fails, try running the CLI directly with bun ./cli/index.ts
.
Run the prune
command to delete archives that don't exist anymore in the file system.
You can generate all covers and page thumbnails using the generate-images
command. Useful if you don't want or can't generate the images on-demand.
Use the --ids <IDs...>
option to indicate which archives it should generate images for.
-f
or --force
can be used to re-generate images.
You can scrape sites for metadata. Use bun cli metadata:scrape <site>
. Currently, only hentag
is supported.
Use the --ids <IDs...>
option to indicate which archives it should scrape metadata for.
With --sleep <time>
you can indicate (in milliseconds) how much time to wait between site requests. By default it's 5 seconds.
You can also use -y
or --no-interact
to skip any prompts. If there are multiple results from the scraper, the best one will be chosen by comparing its titles. If not specified, you will be prompted to select from a list.
You can generate a one-time login link for any user using the uli <username>
command.
Send an access recovery email to a user with the recovery <username>
command. If you only want to get the code without sending an email, use the -c
or --code
option.
You can migrate the old resampled images to the new folder structure.
Use the migrate:images
command.
You need to specify the v1 data directory, the format that will be migrated to the default preset and a connection string for the v1 database.
The image migration must be done before the database migration
Backup your database before continuing
You can migrate the archives from the v1 database to a new SQLite database.
Use the migrate:db
command.
You need to specify a connection string for the v1 database. Example: postgres://user:password@hostname:port/database
.
If you had duplicated paths in the previous database, only the latest non-deleted one will be kept. A list of lost archives will be saved to ./lost_{{timestamp}}.json
.
Once migrated, make a forced index to finish the process: bun ./cli index --force
.
To migrate from a v1 PostgreSQL to a v2 PostgreSQL, you only need to make a forced index.
The same database can be used.