Add librarr app

This commit is contained in:
okxlin
2026-08-01 02:25:29 +08:00
parent d6e84e0306
commit 1d760b6e2d
16 changed files with 599 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
PANEL_APP_PORT_HTTP=5050
APP_DATA_DIR=./data/librarr
APP_EBOOK_DIR=./data/ebooks
APP_AUDIOBOOK_DIR=./data/audiobooks
APP_MANGA_DIR=./data/manga
CONTAINER_NAME=librarr-compose-check
+83
View File
@@ -0,0 +1,83 @@
additionalProperties:
formFields:
- default: 5050
edit: true
envKey: PANEL_APP_PORT_HTTP
labelEn: HTTP Port
labelZh: HTTP 端口
label:
en: HTTP Port
zh: HTTP 端口
zh-Hant: HTTP 埠
ja: HTTP ポート
ko: HTTP 포트
ru: HTTP-порт
ms: Port HTTP
pt-br: Porta HTTP
required: true
rule: paramPort
type: number
- default: ./data/librarr
edit: true
envKey: APP_DATA_DIR
labelEn: Application data directory
labelZh: 应用数据目录
label:
en: Application data directory
zh: 应用数据目录
zh-Hant: 應用資料目錄
ja: アプリケーションデータディレクトリ
ko: 애플리케이션 데이터 디렉터리
ru: Каталог данных приложения
ms: Direktori data aplikasi
pt-br: Diretório de dados do aplicativo
required: true
type: text
- default: ./data/ebooks
edit: true
envKey: APP_EBOOK_DIR
labelEn: Ebook directory
labelZh: 电子书目录
label:
en: Ebook directory
zh: 电子书目录
zh-Hant: 電子書目錄
ja: 電子書籍ディレクトリ
ko: 전자책 디렉터리
ru: Каталог электронных книг
ms: Direktori e-buku
pt-br: Diretório de ebooks
required: true
type: text
- default: ./data/audiobooks
edit: true
envKey: APP_AUDIOBOOK_DIR
labelEn: Audiobook directory
labelZh: 有声书目录
label:
en: Audiobook directory
zh: 有声书目录
zh-Hant: 有聲書目錄
ja: オーディオブックディレクトリ
ko: 오디오북 디렉터리
ru: Каталог аудиокниг
ms: Direktori buku audio
pt-br: Diretório de audiolivros
required: true
type: text
- default: ./data/manga
edit: true
envKey: APP_MANGA_DIR
labelEn: Manga directory
labelZh: 漫画目录
label:
en: Manga directory
zh: 漫画目录
zh-Hant: 漫畫目錄
ja: 漫画ディレクトリ
ko: 만화 디렉터리
ru: Каталог манги
ms: Direktori manga
pt-br: Diretório de mangás
required: true
type: text
+39
View File
@@ -0,0 +1,39 @@
services:
librarr:
image: "ghcr.io/jeremiahm37/librarr:v1.1.3"
container_name: ${CONTAINER_NAME}
restart: always
read_only: true
tmpfs:
- /tmp:size=512M
security_opt:
- no-new-privileges:true
networks:
- 1panel-network
ports:
- "${PANEL_APP_PORT_HTTP}:5050"
volumes:
- "${APP_DATA_DIR}:/data"
- "${APP_EBOOK_DIR}:/books/ebooks"
- "${APP_AUDIOBOOK_DIR}:/books/audiobooks"
- "${APP_MANGA_DIR}:/books/manga"
environment:
- LIBRARR_PORT=5050
- LIBRARR_DB_PATH=/data/librarr.db
- EBOOK_DIR=/books/ebooks
- AUDIOBOOK_DIR=/books/audiobooks
- MANGA_DIR=/books/manga
- WISHLIST_CLEANUP_ENABLED=false
- OIDC_PROXY_HEADERS_ENABLED=false
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:5050/api/health"]
interval: 10s
timeout: 5s
retries: 9
start_period: 15s
labels:
createdBy: "Apps"
networks:
1panel-network:
external: true
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}"
LIBRARR_UID=1000
LIBRARR_GID=1000
fail() {
printf '%s\n' "$1" >&2
exit 1
}
read_env_value() {
local key="$1"
local value=""
value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)"
case "$value" in
\"*\") value="${value#\"}"; value="${value%\"}" ;;
\'*\') value="${value#\'}"; value="${value%\'}" ;;
esac
printf '%s\n' "$value"
}
reject_symlink_components() {
local path="$1"
local current=""
local component
IFS='/' read -r -a components <<< "${path#/}"
for component in "${components[@]}"; do
[[ -n "$component" ]] || continue
current="${current}/${component}"
[[ ! -L "$current" ]] || fail "$path must not contain symbolic-link components"
done
}
prepare_media_dir() {
local key="$1"
local raw=""
local candidate=""
local resolved=""
local scope=""
local existed=false
raw="$(read_env_value "$key")"
[[ -n "$raw" ]] || fail "$key must not be empty"
[[ "$raw" =~ ^[A-Za-z0-9._/\ -]+$ ]] || fail "$key contains unsupported characters"
if [[ "$raw" == /* ]]; then
candidate="$raw"
scope="absolute"
else
candidate="${ROOT_DIR}/${raw#./}"
scope="application"
fi
reject_symlink_components "$candidate"
resolved="$(realpath -m -- "$candidate")" || fail "Unable to resolve $key"
[[ "$resolved" != "/" ]] || fail "$key must not be the filesystem root"
if [[ "$scope" == "application" ]]; then
case "$resolved" in
"${ROOT_DIR}"/*) ;;
*) fail "Relative $key must stay inside the application version directory" ;;
esac
fi
[[ ! -e "$resolved" || -d "$resolved" ]] || fail "$key must be a directory"
[[ -d "$resolved" ]] && existed=true
if [[ "$scope" == "absolute" && "$existed" == "true" ]]; then
local incompatible_owner=""
incompatible_owner="$(find "$resolved" -xdev \( ! -uid "$LIBRARR_UID" -o ! -gid "$LIBRARR_GID" \) -print -quit)" || \
fail "Unable to inspect $key ownership"
[[ -z "$incompatible_owner" ]] || fail "Existing absolute $key tree must be owned by ${LIBRARR_UID}:${LIBRARR_GID}"
fi
install -d -m 0750 -o "$LIBRARR_UID" -g "$LIBRARR_GID" -- "$resolved" || \
fail "Unable to prepare $key"
local verified=""
verified="$(realpath -e -- "$resolved")" || fail "Unable to verify $key"
[[ "$verified" == "$resolved" ]] || fail "$key changed while it was being prepared"
if [[ "$scope" == "application" ]]; then
case "$verified" in
"${ROOT_DIR}"/*) ;;
*) fail "$key resolves outside the application version directory" ;;
esac
chown -R --no-dereference "$LIBRARR_UID:$LIBRARR_GID" "$verified" || \
fail "Unable to set $key ownership"
elif [[ "$existed" == "false" ]]; then
chown "$LIBRARR_UID:$LIBRARR_GID" "$verified" || fail "Unable to set $key ownership"
fi
chmod 0750 "$verified" || fail "Unable to set $key permissions"
printf '%s\n' "$verified"
}
[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found"
[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link"
[[ "$(id -u)" -eq 0 ]] || fail "Librarr init must run as root to prepare media-directory ownership"
data_dir="$(prepare_media_dir APP_DATA_DIR)"
ebook_dir="$(prepare_media_dir APP_EBOOK_DIR)"
audiobook_dir="$(prepare_media_dir APP_AUDIOBOOK_DIR)"
manga_dir="$(prepare_media_dir APP_MANGA_DIR)"
[[ "$data_dir" != "$ebook_dir" && "$data_dir" != "$audiobook_dir" && "$data_dir" != "$manga_dir" && \
"$ebook_dir" != "$audiobook_dir" && "$ebook_dir" != "$manga_dir" && "$audiobook_dir" != "$manga_dir" ]] || \
fail "Librarr data and media directories must be distinct"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Media directories contain user-managed files and are never deleted here.
exit 0
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Librarr applies SQLite migrations in the upstream binary during startup.
exit 0
+48
View File
@@ -0,0 +1,48 @@
## 产品介绍
**Librarr** 是一个面向电子书、有声书和漫画的自托管媒体管理器。它提供统一的 Web 界面,用于管理本地媒体库、愿望单、搜索和下载流程。
## 主要功能
- 分别管理电子书、有声书和漫画目录。
- 维护愿望单、阅读记录、标签和作者监控。
- 支持上传本地媒体文件并整理到对应媒体目录。
- 可按需连接 qBittorrent、Audiobookshelf、Kavita、Komga 等外部服务。
## 访问说明
- 安装完成后打开 Web 端口。
- 首次访问时注册管理员账号;首个用户会自动成为管理员。
- 媒体目录默认为应用版本目录下的 `data/ebooks`、`data/audiobooks` 和 `data/manga`,也可在安装表单中改为其他宿主机路径。
## 数据与升级
- 用户、愿望单、设置和活动记录保存在应用数据目录中的 SQLite 数据库里。
- 三类媒体文件保存在安装表单所选的宿主机目录中。
- 应用限制为单实例,以避免多个实例争用同一数据目录。
- 卸载脚本不会删除媒体目录。卸载或升级前请备份数据库卷和媒体目录。
- `latest` 为浮动镜像,适合通过容器更新工具跟踪上游发布;固定版本用于可重复部署。
## 默认工作流
首次注册管理员后,可以添加和删除愿望单条目,也可以上传一个有效的电子书文件并在上传记录中查看。该流程可用于确认身份验证、SQLite 持久化、媒体目录写入和重启恢复均正常。
## 参考资料
- 项目仓库:<https://github.com/JeremiahM37/librarr>
- 容器镜像:<https://github.com/JeremiahM37/librarr/pkgs/container/librarr>
## Introduction
Librarr is a self-hosted media manager for ebooks, audiobooks, and manga. It provides a unified Web interface for local libraries, wishlists, search, and download workflows.
## Features
- Manage separate ebook, audiobook, and manga directories.
- Maintain wishlists, reading history, tags, and monitored authors.
- Upload local media files and organize them into the selected directories.
- Optionally integrate qBittorrent, Audiobookshelf, Kavita, Komga, and other services.
## First use
Open the Web port and register the first account, which becomes the administrator. The application data path and three media paths can be selected during installation.
+33
View File
@@ -0,0 +1,33 @@
## Introduction
**Librarr** is a self-hosted media manager for ebooks, audiobooks, and manga. It provides a unified Web interface for managing local libraries, wishlists, search, and download workflows.
## Features
- Manage separate ebook, audiobook, and manga directories.
- Maintain wishlists, reading history, tags, and monitored authors.
- Upload local media files and organize them into the selected directories.
- Optionally integrate qBittorrent, Audiobookshelf, Kavita, Komga, and other services.
## First use
- Open the Web port after installation.
- Register the administrator account on first use. The first user automatically becomes an administrator.
- Media directories default to `data/ebooks`, `data/audiobooks`, and `data/manga` under the application version directory. You can select other host paths in the installation form.
## Data and upgrades
- Users, wishlists, settings, and activity are stored in a SQLite database in the selected application data directory.
- Media files are stored in the three host directories selected during installation.
- This package is limited to one instance to prevent multiple installations from competing for the same data directory.
- The uninstall script does not remove media directories. Back up both the database volume and media directories before uninstalling or upgrading.
- `latest` is intentionally movable for users who track releases with container update tools; the numbered version provides a reproducible deployment.
## Default workflow
After registering the first administrator, add and remove a wishlist item, then upload a valid ebook and view its upload record. This exercises authentication, SQLite persistence, media-directory writes, and recovery after a restart.
## Links
- Project: <https://github.com/JeremiahM37/librarr>
- Container image: <https://github.com/JeremiahM37/librarr/pkgs/container/librarr>
+30
View File
@@ -0,0 +1,30 @@
name: Librarr
tags:
- Media
title: 自托管的电子书、有声书和漫画管理器
description: 自托管的电子书、有声书和漫画管理器
additionalProperties:
key: librarr
name: Librarr
description:
en: A self-hosted manager for ebooks, audiobooks, and manga
zh: 自托管的电子书、有声书和漫画管理器
zh-Hant: 自託管的電子書、有聲書和漫畫管理器
ja: 電子書籍、オーディオブック、漫画向けのセルフホスト型管理ツール
ko: 전자책, 오디오북 및 만화를 위한 자체 호스팅 관리자
ru: Самостоятельно размещаемый менеджер электронных и аудиокниг и манги
ms: Pengurus hos sendiri untuk e-buku, buku audio dan manga
pt-br: Gerenciador auto-hospedado de ebooks, audiolivros e mangás
tags:
- Media
shortDescZh: 自托管的电子书、有声书和漫画管理器
shortDescEn: A self-hosted manager for ebooks, audiobooks, and manga
type: tool
crossVersionUpdate: true
limit: 1
recommend: 0
website: https://github.com/JeremiahM37/librarr
github: https://github.com/JeremiahM37/librarr
document: https://github.com/JeremiahM37/librarr#readme
architectures:
- amd64
+6
View File
@@ -0,0 +1,6 @@
PANEL_APP_PORT_HTTP=5050
APP_DATA_DIR=./data/librarr
APP_EBOOK_DIR=./data/ebooks
APP_AUDIOBOOK_DIR=./data/audiobooks
APP_MANGA_DIR=./data/manga
CONTAINER_NAME=librarr-compose-check
+83
View File
@@ -0,0 +1,83 @@
additionalProperties:
formFields:
- default: 5050
edit: true
envKey: PANEL_APP_PORT_HTTP
labelEn: HTTP Port
labelZh: HTTP 端口
label:
en: HTTP Port
zh: HTTP 端口
zh-Hant: HTTP 埠
ja: HTTP ポート
ko: HTTP 포트
ru: HTTP-порт
ms: Port HTTP
pt-br: Porta HTTP
required: true
rule: paramPort
type: number
- default: ./data/librarr
edit: true
envKey: APP_DATA_DIR
labelEn: Application data directory
labelZh: 应用数据目录
label:
en: Application data directory
zh: 应用数据目录
zh-Hant: 應用資料目錄
ja: アプリケーションデータディレクトリ
ko: 애플리케이션 데이터 디렉터리
ru: Каталог данных приложения
ms: Direktori data aplikasi
pt-br: Diretório de dados do aplicativo
required: true
type: text
- default: ./data/ebooks
edit: true
envKey: APP_EBOOK_DIR
labelEn: Ebook directory
labelZh: 电子书目录
label:
en: Ebook directory
zh: 电子书目录
zh-Hant: 電子書目錄
ja: 電子書籍ディレクトリ
ko: 전자책 디렉터리
ru: Каталог электронных книг
ms: Direktori e-buku
pt-br: Diretório de ebooks
required: true
type: text
- default: ./data/audiobooks
edit: true
envKey: APP_AUDIOBOOK_DIR
labelEn: Audiobook directory
labelZh: 有声书目录
label:
en: Audiobook directory
zh: 有声书目录
zh-Hant: 有聲書目錄
ja: オーディオブックディレクトリ
ko: 오디오북 디렉터리
ru: Каталог аудиокниг
ms: Direktori buku audio
pt-br: Diretório de audiolivros
required: true
type: text
- default: ./data/manga
edit: true
envKey: APP_MANGA_DIR
labelEn: Manga directory
labelZh: 漫画目录
label:
en: Manga directory
zh: 漫画目录
zh-Hant: 漫畫目錄
ja: 漫画ディレクトリ
ko: 만화 디렉터리
ru: Каталог манги
ms: Direktori manga
pt-br: Diretório de mangás
required: true
type: text
+39
View File
@@ -0,0 +1,39 @@
services:
librarr:
image: "ghcr.io/jeremiahm37/librarr:latest"
container_name: ${CONTAINER_NAME}
restart: always
read_only: true
tmpfs:
- /tmp:size=512M
security_opt:
- no-new-privileges:true
networks:
- 1panel-network
ports:
- "${PANEL_APP_PORT_HTTP}:5050"
volumes:
- "${APP_DATA_DIR}:/data"
- "${APP_EBOOK_DIR}:/books/ebooks"
- "${APP_AUDIOBOOK_DIR}:/books/audiobooks"
- "${APP_MANGA_DIR}:/books/manga"
environment:
- LIBRARR_PORT=5050
- LIBRARR_DB_PATH=/data/librarr.db
- EBOOK_DIR=/books/ebooks
- AUDIOBOOK_DIR=/books/audiobooks
- MANGA_DIR=/books/manga
- WISHLIST_CLEANUP_ENABLED=false
- OIDC_PROXY_HEADERS_ENABLED=false
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:5050/api/health"]
interval: 10s
timeout: 5s
retries: 9
start_period: 15s
labels:
createdBy: "Apps"
networks:
1panel-network:
external: true
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}"
LIBRARR_UID=1000
LIBRARR_GID=1000
fail() {
printf '%s\n' "$1" >&2
exit 1
}
read_env_value() {
local key="$1"
local value=""
value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)"
case "$value" in
\"*\") value="${value#\"}"; value="${value%\"}" ;;
\'*\') value="${value#\'}"; value="${value%\'}" ;;
esac
printf '%s\n' "$value"
}
reject_symlink_components() {
local path="$1"
local current=""
local component
IFS='/' read -r -a components <<< "${path#/}"
for component in "${components[@]}"; do
[[ -n "$component" ]] || continue
current="${current}/${component}"
[[ ! -L "$current" ]] || fail "$path must not contain symbolic-link components"
done
}
prepare_media_dir() {
local key="$1"
local raw=""
local candidate=""
local resolved=""
local scope=""
local existed=false
raw="$(read_env_value "$key")"
[[ -n "$raw" ]] || fail "$key must not be empty"
[[ "$raw" =~ ^[A-Za-z0-9._/\ -]+$ ]] || fail "$key contains unsupported characters"
if [[ "$raw" == /* ]]; then
candidate="$raw"
scope="absolute"
else
candidate="${ROOT_DIR}/${raw#./}"
scope="application"
fi
reject_symlink_components "$candidate"
resolved="$(realpath -m -- "$candidate")" || fail "Unable to resolve $key"
[[ "$resolved" != "/" ]] || fail "$key must not be the filesystem root"
if [[ "$scope" == "application" ]]; then
case "$resolved" in
"${ROOT_DIR}"/*) ;;
*) fail "Relative $key must stay inside the application version directory" ;;
esac
fi
[[ ! -e "$resolved" || -d "$resolved" ]] || fail "$key must be a directory"
[[ -d "$resolved" ]] && existed=true
if [[ "$scope" == "absolute" && "$existed" == "true" ]]; then
local incompatible_owner=""
incompatible_owner="$(find "$resolved" -xdev \( ! -uid "$LIBRARR_UID" -o ! -gid "$LIBRARR_GID" \) -print -quit)" || \
fail "Unable to inspect $key ownership"
[[ -z "$incompatible_owner" ]] || fail "Existing absolute $key tree must be owned by ${LIBRARR_UID}:${LIBRARR_GID}"
fi
install -d -m 0750 -o "$LIBRARR_UID" -g "$LIBRARR_GID" -- "$resolved" || \
fail "Unable to prepare $key"
local verified=""
verified="$(realpath -e -- "$resolved")" || fail "Unable to verify $key"
[[ "$verified" == "$resolved" ]] || fail "$key changed while it was being prepared"
if [[ "$scope" == "application" ]]; then
case "$verified" in
"${ROOT_DIR}"/*) ;;
*) fail "$key resolves outside the application version directory" ;;
esac
chown -R --no-dereference "$LIBRARR_UID:$LIBRARR_GID" "$verified" || \
fail "Unable to set $key ownership"
elif [[ "$existed" == "false" ]]; then
chown "$LIBRARR_UID:$LIBRARR_GID" "$verified" || fail "Unable to set $key ownership"
fi
chmod 0750 "$verified" || fail "Unable to set $key permissions"
printf '%s\n' "$verified"
}
[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found"
[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link"
[[ "$(id -u)" -eq 0 ]] || fail "Librarr init must run as root to prepare media-directory ownership"
data_dir="$(prepare_media_dir APP_DATA_DIR)"
ebook_dir="$(prepare_media_dir APP_EBOOK_DIR)"
audiobook_dir="$(prepare_media_dir APP_AUDIOBOOK_DIR)"
manga_dir="$(prepare_media_dir APP_MANGA_DIR)"
[[ "$data_dir" != "$ebook_dir" && "$data_dir" != "$audiobook_dir" && "$data_dir" != "$manga_dir" && \
"$ebook_dir" != "$audiobook_dir" && "$ebook_dir" != "$manga_dir" && "$audiobook_dir" != "$manga_dir" ]] || \
fail "Librarr data and media directories must be distinct"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Media directories contain user-managed files and are never deleted here.
exit 0
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Librarr applies SQLite migrations in the upstream binary during startup.
exit 0
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB