From 06e93fd7bdac1544ad3dcae141c74e0bcc8368b7 Mon Sep 17 00:00:00 2001 From: achmad Date: Sun, 10 May 2026 11:45:24 +0700 Subject: [PATCH] chore: add Docker build config, scripts, and private registry setup --- .DS_Store | Bin 0 -> 6148 bytes API_DOCUMENTATION.md | 1734 ++++++++++++++++++++++++++++++++ Dockerfile | 105 ++ docker-compose-postgresql.yml | 71 ++ docker-compose.yml | 43 + scripts/create_server_conf.sh | 14 + scripts/kcef_download.sh | 83 ++ scripts/startup_script.sh | 178 ++++ scripts/tachidesk_version.json | 3 + suwayomi-server.container | 26 + 10 files changed, 2257 insertions(+) create mode 100644 .DS_Store create mode 100644 API_DOCUMENTATION.md create mode 100644 Dockerfile create mode 100644 docker-compose-postgresql.yml create mode 100644 docker-compose.yml create mode 100644 scripts/create_server_conf.sh create mode 100644 scripts/kcef_download.sh create mode 100644 scripts/startup_script.sh create mode 100644 scripts/tachidesk_version.json create mode 100644 suwayomi-server.container diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..02ef7264dcd5538c9f53579f4458c373204f90a6 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O0Z74zx3Oz1(Ef|Yfix6uoA$_Q&*z6ny^iR)y+udN`(3vq z4*K22!fx*D?j4_v9+Rh3zGy-@@U3LaUoq>F$eggRp27Z~^eyShqU literal 0 HcmV?d00001 diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 00000000..6ee514c6 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,1734 @@ +# Suwayomi-Server REST API Documentation + +This document describes all REST API endpoints available in Suwayomi-Server. + +**Base URL:** `http://localhost:4567/api/v1/` + +--- + +## Authentication + +All API endpoints (except where noted) require HTTP Basic Authentication. + +### Authorization Header + +Include the following header in your requests: + +``` +Authorization: Basic +``` + +### How to Generate the Header + +1. Combine username and password with a colon: `username:password` +2. Encode the string in Base64 +3. Prepend with `Basic ` + +**Example:** +- Username: `admin` +- Password: `password123` +- Credentials string: `admin:password123` +- Base64 encoded: `YWRtaW46cGFzc3dvcmQxMjM=` +- Header: `Authorization: Basic YWRtaW46cGFzc3dvcmQxMjM=` + +### Default Credentials + +The default username and password are configured in the server settings: +- Default username: `admin` +- Default password: `admin` + +**Note:** These should be changed in production via server configuration. + +### Authentication Modes + +The server supports different authentication modes (configured in server settings): + +1. **`BASIC_AUTH`** - Requires Basic Auth header for all API requests +2. **`SIMPLE_LOGIN`** - Uses cookie-based session authentication for web interface, but requires Basic Auth for API +3. **`NONE`** - No authentication required (not recommended for production) + +### Unauthorized Response + +If authentication fails, the server returns: + +**HTTP 401 Unauthorized** +``` +WWW-Authenticate: Basic +``` + +**Response Body:** +```json +{ + "error": "Unauthorized", + "message": "Authentication required" +} +``` + +### WebSocket Authentication + +WebSocket endpoints use the same Basic Authentication mechanism. Include the Authorization header when establishing the WebSocket connection. + +--- + +## Table of Contents + +1. [Extension Endpoints](#extension-endpoints) +2. [Source Endpoints](#source-endpoints) +3. [Manga Endpoints](#manga-endpoints) +4. [Chapter Endpoints](#chapter-endpoints) +5. [Category Endpoints](#category-endpoints) +6. [Backup Endpoints](#backup-endpoints) +7. [Download Endpoints](#download-endpoints) +8. [Update Endpoints](#update-endpoints) +9. [Track Endpoints](#track-endpoints) +10. [Global Meta Endpoints](#global-meta-endpoints) +11. [Settings Endpoints](#settings-endpoints) +12. [WebView Endpoints](#webview-endpoints) +13. [WebSocket Endpoints](#websocket-endpoints) +14. [OPDS Endpoints](#opds-endpoints) + +--- + +## Extension Endpoints + +### `GET /api/v1/extension/list` + +- **What it does:** List all installed extensions +- **Expected request:** No parameters required +- **Expected response:** JSON array of ExtensionDataClass objects (HTTP 200) + +**Response JSON Example:** +```json +[ + { + "repo": "https://raw.githubusercontent.com/k对身体1/extensions/repo", + "apkName": "extensions/kofthub/Dramacafe", + "iconUrl": "/extension/icon/Dramacafe.apk", + "name": "Dramacafe", + "pkgName": "eu.kanade.tachiyomi.extension.en.dramacafe", + "versionName": "1.2.3", + "versionCode": 12, + "lang": "en", + "isNsfw": false, + "installed": true, + "hasUpdate": false, + "obsolete": false + } +] +``` + +### `GET /api/v1/extension/install/{pkgName}` + +- **What it does:** Install an extension by package name +- **Expected request:** Path parameter: `pkgName` (string) - e.g., `eu.kanade.tachiyomi.extension.en.mangadex` +- **Expected response:** HTTP 201 (Created), HTTP 302 (Found), or HTTP 500 (Internal Server Error) + +### `POST /api/v1/extension/install` + +- **What it does:** Install an extension from uploaded APK file +- **Expected request:** Multipart form data with file field named "file" containing the extension APK +- **Expected response:** HTTP 201 (Created), HTTP 302 (Found), or HTTP 500 (Internal Server Error) + +### `GET /api/v1/extension/update/{pkgName}` + +- **What it does:** Update an extension by package name +- **Expected request:** Path parameter: `pkgName` (string) +- **Expected response:** HTTP 201 (Created), HTTP 302 (Found), HTTP 404 (Not Found), or HTTP 500 (Internal Server Error) + +### `GET /api/v1/extension/uninstall/{pkgName}` + +- **What it does:** Uninstall an extension by package name +- **Expected request:** Path parameter: `pkgName` (string) +- **Expected response:** HTTP 200 (OK), HTTP 404 (Not Found), or HTTP 500 (Internal Server Error) + +### `GET /api/v1/extension/icon/{apkName}` + +- **What it does:** Get the icon for an extension +- **Expected request:** Path parameter: `apkName` (string) - e.g., `Dramacafe.apk` +- **Expected response:** Image file (PNG/JPEG) with HTTP 200, or HTTP 404 (Not Found) + +--- + +## Source Endpoints + +### `GET /api/v1/source/list` + +- **What it does:** List all available sources +- **Expected request:** No parameters required +- **Expected response:** JSON array of SourceDataClass objects (HTTP 200) + +**Response JSON Example:** +```json +[ + { + "id": "1234567890", + "name": "MangaDex", + "lang": "en", + "iconUrl": "/source/icon/1234567890", + "supportsLatest": true, + "isConfigurable": true, + "isNsfw": false, + "displayName": "MangaDex", + "baseUrl": "https://mangadex.org" + } +] +``` + +### `GET /api/v1/source/{sourceId}` + +- **What it does:** Get information about a specific source +- **Expected request:** Path parameter: `sourceId` (long) - e.g., `1234567890` +- **Expected response:** JSON SourceDataClass object (HTTP 200) or HTTP 404 (Not Found) + +### `GET /api/v1/source/{sourceId}/popular/{pageNum}` + +- **What it does:** Get popular manga from a source +- **Expected request:** Path parameters: `sourceId` (long), `pageNum` (int) +- **Expected response:** JSON PagedMangaListDataClass object (HTTP 200) + +**Response JSON Example:** +```json +{ + "mangaList": [ + { + "id": 1, + "sourceId": "1234567890", + "url": "/manga/12345", + "title": "One Piece", + "thumbnailUrl": "https://example.com/cover.jpg", + "thumbnailUrlLastFetched": 1699999999, + "initialized": true, + "artist": "Eiichiro Oda", + "author": "Eiichiro Oda", + "description": "Monkey D. Luffy sets off on an adventure...", + "genre": ["Action", "Adventure", "Comedy"], + "status": "ONGOING", + "inLibrary": true, + "inLibraryAt": 1699999999, + "source": null, + "meta": {}, + "realUrl": "https://mangadex.org/manga/12345", + "lastFetchedAt": 1699999999, + "chaptersLastFetchedAt": 1699999999, + "updateStrategy": "ALWAYS_UPDATE", + "freshData": false, + "unreadCount": 100, + "downloadCount": 0, + "chapterCount": 1000, + "lastReadAt": null, + "lastChapterRead": null, + "age": 0, + "chaptersAge": 0, + "trackers": null + } + ], + "hasNextPage": true +} +``` + +### `GET /api/v1/source/{sourceId}/latest/{pageNum}` + +- **What it does:** Get latest manga from a source +- **Expected request:** Path parameters: `sourceId` (long), `pageNum` (int) +- **Expected response:** JSON PagedMangaListDataClass object (HTTP 200) + +### `GET /api/v1/source/{sourceId}/preferences` + +- **What it does:** Get source preferences/settings +- **Expected request:** Path parameter: `sourceId` (long) +- **Expected response:** JSON array of preference objects (HTTP 200) + +**Response JSON Example:** +```json +[ + { + "type": "EditTextPreference", + "props": { + "key": "username", + "title": "Username", + "summary": "", + "text": "", + "default": "" + } + }, + { + "type": "SwitchPreferenceCompat", + "props": { + "key": "hentai", + "title": "Show Hentai", + "summary": "Show mature content", + "checked": false + } + } +] +``` + +### `POST /api/v1/source/{sourceId}/preferences` + +- **What it does:** Set a source preference +- **Expected request:** Path parameter: `sourceId` (long), body: SourcePreferenceChange JSON object + +**Request JSON Example:** +```json +{ + "position": 0, + "value": "myusername" +} +``` + +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/source/{sourceId}/filters` + +- **What it does:** Get source filters +- **Expected request:** Path parameter: `sourceId` (long), optional query param: `reset` (boolean) +- **Expected response:** JSON array of FilterObject objects (HTTP 200) + +**Response JSON Example:** +```json +[ + { + "type": "Header", + "filter": { + "name": "Content" + } + }, + { + "type": "Select", + "filter": { + "name": "Genre", + "state": 0, + "values": ["All", "Action", "Adventure", "Comedy"] + } + }, + { + "type": "Text", + "filter": { + "name": "Year", + "state": "" + } + }, + { + "type": "CheckBox", + "filter": { + "name": "Completed", + "state": false + } + } +] +``` + +### `POST /api/v1/source/{sourceId}/filters` + +- **What it does:** Set source filters +- **Expected request:** Path parameter: `sourceId` (long), body: FilterChange object or array of FilterChange objects + +**Request JSON Example (Single filter):** +```json +{ + "position": 1, + "state": "2" +} +``` + +**Request JSON Example (Multiple filters):** +```json +[ + { "position": 1, "state": "2" }, + { "position": 2, "state": "2023" }, + { "position": 3, "state": "true" } +] +``` + +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/source/{sourceId}/search` + +- **What it does:** Search for manga in a single source +- **Expected request:** Path parameter: `sourceId` (long), query params: `searchTerm` (string), `pageNum` (int, default: 1) +- **Expected response:** JSON PagedMangaListDataClass object (HTTP 200) + +### `POST /api/v1/source/{sourceId}/quick-search` + +- **What it does:** Quick search manga in a source using filters +- **Expected request:** Path parameter: `sourceId` (long), query param: `pageNum` (int, default: 1), body: FilterData object + +**Request JSON Example:** +```json +{ + "searchTerm": "One Piece", + "filter": [ + { "position": 1, "state": "0" }, + { "position": 2, "state": "" } + ] +} +``` + +- **Expected response:** JSON PagedMangaListDataClass object (HTTP 200) + +--- + +## Manga Endpoints + +### `GET /api/v1/manga/{mangaId}` + +- **What it does:** Get manga information +- **Expected request:** Path parameter: `mangaId` (int), optional query param: `onlineFetch` (boolean) +- **Expected response:** JSON MangaDataClass object (HTTP 200) or HTTP 404 (Not Found) + +**Response JSON Example:** +```json +{ + "id": 1, + "sourceId": "1234567890", + "url": "/manga/12345", + "title": "One Piece", + "thumbnailUrl": "https://example.com/cover.jpg", + "thumbnailUrlLastFetched": 1699999999, + "initialized": true, + "artist": "Eiichiro Oda", + "author": "Eiichiro Oda", + "description": "Monkey D. Luffy sets off on an adventure...", + "genre": ["Action", "Adventure", "Comedy", "Fantasy"], + "status": "ONGOING", + "inLibrary": true, + "inLibraryAt": 1699999999, + "source": { + "id": "1234567890", + "name": "MangaDex", + "lang": "en", + "iconUrl": "/source/icon/1234567890", + "supportsLatest": true, + "isConfigurable": true, + "isNsfw": false, + "displayName": "MangaDex", + "baseUrl": "https://mangadex.org" + }, + "meta": {}, + "realUrl": "https://mangadex.org/manga/12345", + "lastFetchedAt": 1699999999, + "chaptersLastFetchedAt": 1699999999, + "updateStrategy": "ALWAYS_UPDATE", + "freshData": false, + "unreadCount": 100, + "downloadCount": 0, + "chapterCount": 1000, + "lastReadAt": 1699999999, + "lastChapterRead": { + "id": 100, + "url": "/chapter/12345/1", + "name": "Chapter 1: Dawn of Adventure", + "uploadDate": 1699999999, + "chapterNumber": 1.0, + "scanlator": "MangaDex", + "mangaId": 1, + "read": true, + "bookmarked": false, + "lastPageRead": 20, + "lastReadAt": 1699999999, + "index": 1, + "fetchedAt": 1699999999, + "realUrl": "https://mangadex.org/chapter/12345/1", + "downloaded": true, + "pageCount": 20, + "chapterCount": 1000, + "meta": {} + }, + "age": 0, + "chaptersAge": 0, + "trackers": [ + { + "trackerId": 1, + "trackerName": "MyAnimeList", + "remoteId": "12345", + "remoteUrl": "https://myanimelist.net/manga/12345", + "title": "One Piece", + "lastChapterRead": 100, + "totalChapters": 1000, + "score": 9.5, + "status": "Reading", + "startedReadingDate": null, + "finishedReadingDate": null, + "private": false + } + ] +} +``` + +### `GET /api/v1/manga/{mangaId}/full` + +- **What it does:** Get full manga information with all data filled in +- **Expected request:** Path parameter: `mangaId` (int), optional query param: `onlineFetch` (boolean) +- **Expected response:** JSON MangaDataClass object (HTTP 200) or HTTP 404 (Not Found) + +### `GET /api/v1/manga/{mangaId}/thumbnail` + +- **What it does:** Get manga thumbnail image +- **Expected request:** Path parameter: `mangaId` (int) +- **Expected response:** Image file (JPEG/PNG) with HTTP 200 or HTTP 404 (Not Found) + +### `GET /api/v1/manga/{mangaId}/category` + +- **What it does:** Get categories assigned to a manga +- **Expected request:** Path parameter: `mangaId` (int) +- **Expected response:** JSON array of CategoryDataClass objects (HTTP 200) + +**Response JSON Example:** +```json +[ + { + "id": 1, + "order": 0, + "name": "Favorites", + "default": false, + "size": 10, + "includeInUpdate": "INCLUDE", + "includeInDownload": "INCLUDE", + "meta": {} + } +] +``` + +### `GET /api/v1/manga/{mangaId}/category/{categoryId}` + +- **What it does:** Add manga to a category +- **Expected request:** Path parameters: `mangaId` (int), `categoryId` (int) +- **Expected response:** HTTP 200 (OK) + +### `DELETE /api/v1/manga/{mangaId}/category/{categoryId}` + +- **What it does:** Remove manga from a category +- **Expected request:** Path parameters: `mangaId` (int), `categoryId` (int) +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/manga/{mangaId}/library` + +- **What it does:** Add manga to library +- **Expected request:** Path parameter: `mangaId` (int) +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +### `DELETE /api/v1/manga/{mangaId}/library` + +- **What it does:** Remove manga from library +- **Expected request:** Path parameter: `mangaId` (int) +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +### `PATCH /api/v1/manga/{mangaId}/meta` + +- **What it does:** Add/update metadata to a manga +- **Expected request:** Path parameter: `mangaId` (int), form params: `key` (string), `value` (string) +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +### `GET /api/v1/manga/{mangaId}/chapters` + +- **What it does:** Get manga chapter list +- **Expected request:** Path parameter: `mangaId` (int), optional query param: `onlineFetch` (boolean) +- **Expected response:** JSON array of ChapterDataClass objects (HTTP 200) or HTTP 404 (Not Found) + +**Response JSON Example:** +```json +[ + { + "id": 100, + "url": "/chapter/12345/1000", + "name": "Chapter 1000: The Final Battle", + "uploadDate": 1699999999, + "chapterNumber": 1000.0, + "scanlator": "MangaDex", + "mangaId": 1, + "read": false, + "bookmarked": false, + "lastPageRead": 0, + "lastReadAt": 0, + "index": 1000, + "fetchedAt": 1699999999, + "realUrl": "https://mangadex.org/chapter/12345/1000", + "downloaded": false, + "pageCount": 18, + "chapterCount": 1000, + "meta": {} + }, + { + "id": 99, + "url": "/chapter/12345/999", + "name": "Chapter 999: The Beginning", + "uploadDate": 1699999998, + "chapterNumber": 999.0, + "scanlator": "MangaDex", + "mangaId": 1, + "read": true, + "bookmarked": true, + "lastPageRead": 15, + "lastReadAt": 1699999999, + "index": 999, + "fetchedAt": 1699999998, + "realUrl": "https://mangadex.org/chapter/12345/999", + "downloaded": true, + "pageCount": 15, + "chapterCount": 1000, + "meta": {} + } +] +``` + +### `POST /api/v1/manga/{mangaId}/chapter/batch` + +- **What it does:** Batch update chapters (mark as read, bookmark, etc.) +- **Expected request:** Path parameter: `mangaId` (int), body: MangaChapterBatchEditInput object + +**Request JSON Example (Mark as read and bookmark):** +```json +{ + "chapterIds": [100, 101, 102], + "chapterIndexes": null, + "change": { + "isRead": true, + "isBookmarked": true, + "lastPageRead": null, + "delete": null + } +} +``` + +**Request JSON Example (Mark previous as read):** +```json +{ + "chapterIds": [100], + "chapterIndexes": null, + "change": { + "isRead": null, + "isBookmarked": null, + "lastPageRead": null, + "delete": null, + "markPrevRead": true + } +} +``` + +**Request JSON Example (Delete downloaded chapters):** +```json +{ + "chapterIds": [100, 101], + "change": { + "isRead": null, + "isBookmarked": null, + "lastPageRead": null, + "delete": true + } +} +``` + +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/manga/{mangaId}/chapter/{chapterIndex}` + +- **What it does:** Get a specific chapter with pages +- **Expected request:** Path parameters: `mangaId` (int), `chapterIndex` (int) +- **Expected response:** JSON ChapterDataClass object (HTTP 200) or HTTP 404 (Not Found) + +### `PATCH /api/v1/manga/{mangaId}/chapter/{chapterIndex}` + +- **What it does:** Modify chapter (read status, bookmark, etc.) +- **Expected request:** Path parameters: `mangaId` (int), `chapterIndex` (int), form params: `read` (boolean, optional), `bookmarked` (boolean, optional), `markPrevRead` (boolean, optional), `lastPageRead` (int, optional) + +**Form Data Example:** +``` +read=true +bookmarked=false +lastPageRead=5 +markPrevRead=true +``` + +- **Expected response:** HTTP 200 (OK) + +### `PUT /api/v1/manga/{mangaId}/chapter/{chapterIndex}` + +- **What it does:** Modify chapter (same as PATCH) +- **Expected request:** Same as PATCH +- **Expected response:** HTTP 200 (OK) + +### `DELETE /api/v1/manga/{mangaId}/chapter/{chapterIndex}` + +- **What it does:** Delete downloaded chapter files +- **Expected request:** Path parameters: `mangaId` (int), `chapterIndex` (int) +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +### `PATCH /api/v1/manga/{mangaId}/chapter/{chapterIndex}/meta` + +- **What it does:** Add metadata to a chapter +- **Expected request:** Path parameters: `mangaId` (int), `chapterIndex` (int), form params: `key` (string), `value` (string) +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +### `GET /api/v1/manga/{mangaId}/chapter/{chapterIndex}/page/{index}` + +- **What it does:** Get a chapter page image +- **Expected request:** Path parameters: `mangaId` (int), `chapterIndex` (int), `index` (int), optional query params: `updateProgress` (boolean), `format` (string), `opds` (boolean) +- **Expected response:** Image file with HTTP 200 or HTTP 404 (Not Found) + +--- + +## Chapter Endpoints + +### `POST /api/v1/chapter/batch` + +- **What it does:** Batch update chapters across any manga +- **Expected request:** Body: ChapterBatchEditInput object + +**Request JSON Example:** +```json +{ + "chapterIds": [100, 101, 102, 200, 201], + "change": { + "isRead": true, + "isBookmarked": false, + "lastPageRead": null, + "delete": null + } +} +``` + +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/chapter/{chapterId}/download` + +- **What it does:** Download chapter as CBZ file +- **Expected request:** Path parameter: `chapterId` (int), optional query param: `markAsRead` (boolean) +- **Expected response:** CBZ file stream with HTTP 200 or HTTP 404 (Not Found) + +**Response Headers:** +``` +Content-Type: application/vnd.comicbook+zip +Content-Disposition: attachment; filename="One Piece - Chapter 1000.cbz" +Content-Length: 1234567 +``` + +### `HEAD /api/v1/chapter/{chapterId}/download` + +- **What it does:** Get CBZ file metadata (HEAD request) +- **Expected request:** Path parameter: `chapterId` (int) +- **Expected response:** HTTP headers with file info, or HTTP 404 (Not Found) + +--- + +## Category Endpoints + +### `GET /api/v1/category` + +- **What it does:** Get list of all categories +- **Expected request:** No parameters required +- **Expected response:** JSON array of CategoryDataClass objects (HTTP 200) + +**Response JSON Example:** +```json +[ + { + "id": 1, + "order": 0, + "name": "Favorites", + "default": false, + "size": 10, + "includeInUpdate": "INCLUDE", + "includeInDownload": "INCLUDE", + "meta": {} + }, + { + "id": 2, + "order": 1, + "name": "Reading", + "default": false, + "size": 5, + "includeInUpdate": "INCLUDE", + "includeInDownload": "EXCLUDE", + "meta": {} + }, + { + "id": 3, + "order": 2, + "name": "Default", + "default": true, + "size": 100, + "includeInUpdate": "INCLUDE", + "includeInDownload": "INCLUDE", + "meta": {} + } +] +``` + +### `POST /api/v1/category` + +- **What it does:** Create a new category +- **Expected request:** Form param: `name` (string) + +**Form Data Example:** +``` +name=My Category +``` + +- **Expected response:** HTTP 200 (OK) or HTTP 400 (Bad Request) + +### `PATCH /api/v1/category/reorder` + +- **What it does:** Reorder categories +- **Expected request:** Form params: `from` (int), `to` (int) + +**Form Data Example:** +``` +from=0 +to=2 +``` + +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/category/{categoryId}` + +- **What it does:** Get manga in a category +- **Expected request:** Path parameter: `categoryId` (int) +- **Expected response:** JSON array of MangaDataClass objects (HTTP 200) + +### `PATCH /api/v1/category/{categoryId}` + +- **What it does:** Modify a category +- **Expected request:** Path parameter: `categoryId` (int), optional form params: `name` (string), `default` (boolean), `includeInUpdate` (int), `includeInDownload` (int) + +**Form Data Example:** +``` +name=Updated Name +default=false +includeInUpdate=1 +includeInDownload=0 +``` + +- **Expected response:** HTTP 200 (OK) + +### `DELETE /api/v1/category/{categoryId}` + +- **What it does:** Delete a category +- **Expected request:** Path parameter: `categoryId` (int) +- **Expected response:** HTTP 200 (OK) + +### `PATCH /api/v1/category/{categoryId}/meta` + +- **What it does:** Add metadata to a category +- **Expected request:** Path parameter: `categoryId` (int), form params: `key` (string), `value` (string) +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +--- + +## Backup Endpoints + +### `POST /api/v1/backup/import` + +- **What it does:** Restore a backup from request body +- **Expected request:** Body: Tachiyomi protobuf backup data (binary) +- **Expected response:** HTTP 200 (OK) + +### `POST /api/v1/backup/import/file` + +- **What it does:** Restore a backup from file upload +- **Expected request:** Multipart form data with file named "backup.proto.gz" +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +### `POST /api/v1/backup/validate` + +- **What it does:** Validate a backup from request body +- **Expected request:** Body: Tachiyomi protobuf backup data (binary) +- **Expected response:** JSON ValidationResult object (HTTP 200) + +**Response JSON Example:** +```json +{ + "missingSources": ["com.source.example"], + "missingTrackers": ["myanimelist"], + "isValid": false +} +``` + +### `POST /api/v1/backup/validate/file` + +- **What it does:** Validate a backup from file upload +- **Expected request:** Multipart form data with file named "backup.proto.gz" +- **Expected response:** JSON ValidationResult object (HTTP 200) + +### `GET /api/v1/backup/export` + +- **What it does:** Create a backup (returns as body) +- **Expected request:** No parameters required +- **Expected response:** Binary stream (protobuf) with HTTP 200 + +**Response Headers:** +``` +Content-Type: application/octet-stream +``` + +### `GET /api/v1/backup/export/file` + +- **What it does:** Create a backup (returns as file download) +- **Expected request:** No parameters required +- **Expected response:** Binary stream (protobuf) as file download with HTTP 200 + +**Response Headers:** +``` +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="suwayomi_backup_20240101.proto.gz" +``` + +--- + +## Download Endpoints + +### `GET /api/v1/downloads/start` + +- **What it does:** Start the downloader +- **Expected request:** No parameters required +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/downloads/stop` + +- **What it does:** Stop the downloader +- **Expected request:** No parameters required +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/downloads/clear` + +- **What it does:** Clear the download queue +- **Expected request:** No parameters required +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/download/{mangaId}/chapter/{chapterIndex}` + +- **What it does:** Queue a chapter for download +- **Expected request:** Path parameters: `mangaId` (int), `chapterIndex` (int) +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +### `DELETE /api/v1/download/{mangaId}/chapter/{chapterIndex}` + +- **What it does:** Remove a chapter from download queue +- **Expected request:** Path parameters: `mangaId` (int), `chapterIndex` (int) +- **Expected response:** HTTP 200 (OK) + +### `PATCH /api/v1/download/{mangaId}/chapter/{chapterIndex}/reorder/{to}` + +- **What it does:** Reorder a chapter in download queue +- **Expected request:** Path parameters: `mangaId` (int), `chapterIndex` (int), `to` (int) +- **Expected response:** HTTP 200 (OK) + +### `POST /api/v1/download/batch` + +- **What it does:** Queue multiple chapters for download +- **Expected request:** Body: EnqueueInput object + +**Request JSON Example:** +```json +{ + "chapterIds": [100, 101, 102, 103, 104] +} +``` + +- **Expected response:** HTTP 200 (OK) + +### `DELETE /api/v1/download/batch` + +- **What it does:** Remove multiple chapters from download queue +- **Expected request:** Body: EnqueueInput object + +**Request JSON Example:** +```json +{ + "chapterIds": [100, 101, 102] +} +``` + +- **Expected response:** HTTP 200 (OK) + +--- + +## Update Endpoints + +### `GET /api/v1/update/recentChapters/{pageNum}` + +- **What it does:** Get recently updated chapters +- **Expected request:** Path parameter: `pageNum` (int) +- **Expected response:** JSON PagedMangaChapterListDataClass object (HTTP 200) + +**Response JSON Example:** +```json +{ + "mangaList": [ + { + "id": 1, + "mangaId": 1, + "mangaTitle": "One Piece", + "mangaThumbnail": "https://example.com/cover.jpg", + "chapters": [ + { + "id": 100, + "url": "/chapter/1/100", + "name": "Chapter 100", + "uploadDate": 1699999999, + "chapterNumber": 100.0, + "scanlator": "MangaDex", + "read": false, + "bookmarked": false, + "lastPageRead": 0, + "lastReadAt": 0, + "index": 100, + "fetchedAt": 1699999999, + "downloaded": false + } + ] + } + ], + "hasNextPage": true +} +``` + +### `POST /api/v1/update/fetch` + +- **What it does:** Start library update (optionally for specific category) +- **Expected request:** Optional form param: `categoryId` (int) + +**Form Data Examples:** +``` +(Updates entire library) +``` + +or +``` +categoryId=1 +``` +- **Expected response:** HTTP 200 (OK) or HTTP 400 (Bad Request) + +### `POST /api/v1/update/reset` + +- **What it does:** Stop and reset the updater +- **Expected request:** No parameters required +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/update/summary` + +- **What it does:** Get updater status summary +- **Expected request:** No parameters required +- **Expected response:** JSON UpdateStatus object (HTTP 200) + +**Response JSON Example:** +```json +{ + "status": "Running", + "progress": { + "current": 5, + "total": 100 + }, + "pending": 0, + "running": true, + "mangaDone": 5, + "mangaTotal": 100, + "chapterDone": 50, + "chapterAdded": 10 +} +``` + +--- + +## Track Endpoints + +### `GET /api/v1/track/list` + +- **What it does:** List all supported trackers +- **Expected request:** No parameters required +- **Expected response:** JSON array of TrackerDataClass objects (HTTP 200) + +**Response JSON Example:** +```json +[ + { + "id": 1, + "name": "MyAnimeList", + "icon": "/track/thumbnail/1", + "isLogin": false, + "authUrl": "https://myanimelist.net/api/v2/oauth/authorize" + }, + { + "id": 2, + "name": "AniList", + "icon": "/track/thumbnail/2", + "isLogin": true, + "authUrl": "https://anilist.co/api/v2/oauth/authorize" + } +] +``` + +### `POST /api/v1/track/login` + +- **What it does:** Login to a tracker +- **Expected request:** Body: LoginInput object + +**Request JSON Example:** +```json +{ + "trackerId": 1, + "callbackUrl": "suwayomi://auth/myanimelist", + "username": "myuser", + "password": "mypassword" +} +``` + +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +### `POST /api/v1/track/logout` + +- **What it does:** Logout from a tracker +- **Expected request:** Body: LogoutInput object + +**Request JSON Example:** +```json +{ + "trackerId": 1 +} +``` + +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +### `POST /api/v1/track/search` + +- **What it does:** Search for manga on a tracker +- **Expected request:** Body: SearchInput object + +**Request JSON Example:** +```json +{ + "trackerId": 1, + "title": "One Piece" +} +``` + +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +**Response JSON Example:** +```json +[ + { + "trackerId": 1, + "remoteId": "12345", + "remoteUrl": "https://myanimelist.net/manga/12345", + "title": "One Piece", + "coverUrl": "https://example.com/cover.jpg", + "summary": "A story about pirates...", + "match": 95 + } +] +``` + +### `POST /api/v1/track/bind` + +- **What it does:** Bind a manga to a track record +- **Expected request:** Query params: `mangaId` (int), `trackerId` (int), `remoteId` (string), `private` (boolean) + +**Request URL Example:** +``` +/api/v1/track/bind?mangaId=1&trackerId=1&remoteId=12345&private=false +``` + +- **Expected response:** HTTP 200 (OK) + +### `POST /api/v1/track/update` + +- **What it does:** Update track record with tracker +- **Expected request:** Body: UpdateInput object + +**Request JSON Example (Update reading progress):** +```json +{ + "recordId": 1, + "status": 1, + "lastChapterRead": 100, + "scoreString": "9.5", + "startDate": null, + "finishDate": null, + "private": false +} +``` + +**Request JSON Example (Unbind from tracker):** +```json +{ + "recordId": 1, + "unbind": true +} +``` + +**Status values:** +- 1 = Reading +- 2 = Completed +- 3 = On Hold +- 4 = Dropped +- 6 = Plan to Read + +- **Expected response:** HTTP 200 (OK) + +### `GET /api/v1/track/{trackerId}/thumbnail` + +- **What it does:** Get tracker thumbnail image +- **Expected request:** Path parameter: `trackerId` (int) +- **Expected response:** Image file with HTTP 200 or HTTP 404 (Not Found) + +--- + +## Global Meta Endpoints + +### `GET /api/v1/meta` + +- **What it does:** Get all global metadata +- **Expected request:** No parameters required +- **Expected response:** HTTP 200 (OK) with JSON key-value pairs + +**Response JSON Example:** +```json +{ + "customKey": "customValue", + "lastSync": "1234567890" +} +``` + +### `PATCH /api/v1/meta` + +- **What it does:** Add/update global metadata +- **Expected request:** Form params: `key` (string), `value` (string) + +**Form Data Example:** +``` +key=lastSync +value=1234567890 +``` + +- **Expected response:** HTTP 200 (OK) or HTTP 404 (Not Found) + +--- + +## Settings Endpoints + +### `GET /api/v1/settings/about` + +- **What it does:** Get application information +- **Expected request:** No parameters required +- **Expected response:** JSON AboutDataClass object (HTTP 200) + +**Response JSON Example:** +```json +{ + "name": "Suwayomi-Server", + "version": "0.7.0", + "revision": "7a1b2c3", + "buildType": "release", + "buildTime": 1704067200, + "github": "https://github.com/Suwayomi/Suwayomi-Server", + "discord": "https://discord.gg/Suwayomi" +} +``` + +### `GET /api/v1/settings/check-update` + +- **What it does:** Check for application updates +- **Expected request:** No parameters required +- **Expected response:** JSON array of UpdateDataClass objects (HTTP 200) + +**Response JSON Example:** +```json +[ + { + "name": "0.8.0", + "tagName": "v0.8.0", + "body": "## Bug fixes\n- Fixed bug #123\n- Improved performance", + "htmlUrl": "https://github.com/Suwayomi/Suwayomi-Server/releases/tag/v0.8.0", + "prerelease": false + } +] +``` + +--- + +## WebView Endpoints + +### `GET /api/v1/webview` + +- **What it does:** Open webview interface +- **Expected request:** Optional query param: `lang` (string) + +**Request URL Example:** +``` +/api/v1/webview?lang=en +``` + +- **Expected response:** HTML page (HTTP 200) + +--- + +## WebSocket Endpoints + +The server provides WebSocket connections for real-time communication and updates. + +**Note:** Actions like starting/stopping downloads or updates are done via HTTP endpoints (`GET /api/v1/downloads/start`, `GET /api/v1/downloads/stop`, `POST /api/v1/update/fetch`, etc.), not through WebSocket messages. WebSocket connections are primarily for receiving real-time status updates. + +--- + +### `WS /api/v1/downloads` + +- **What it does:** Real-time download queue status updates +- **Expected request:** WebSocket connection with authentication + +#### Messages You Can Send to Server: + +**Request current download status:** +``` +STATUS +``` +- **Expected Response from Server:** +```json +{ + "status": "Started", + "queue": [ + { + "mangaId": 1, + "chapterId": 100, + "chapterIndex": 100, + "mangaTitle": "One Piece", + "chapterTitle": "Chapter 100", + "state": "DOWNLOADING", + "progress": 50, + "tries": 0, + "downloadSpeed": 1024000, + "errorMessage": null + } + ] +} +``` + +**State values:** `Queued`, `Downloading`, `Completed`, `Error` + +**Invalid command response:** +``` +Invalid command. +Supported commands are: + - STATUS + sends the current download status +``` + +#### Messages Received from Server (Automatic Updates): + +The server automatically sends status updates when: +- A download starts/completes/fails +- Queue changes (add/remove/reorder) + +**Example update:** +```json +{ + "status": "Started", + "queue": [ + { + "mangaId": 1, + "chapterId": 100, + "chapterIndex": 100, + "mangaTitle": "One Piece", + "chapterTitle": "Chapter 100", + "state": "Completed", + "progress": 100, + "tries": 0, + "downloadSpeed": 0, + "errorMessage": null + } + ], + "downloads": [ + { + "downloadQueueItem": { + "mangaId": 1, + "chapterId": 101, + "chapterIndex": 101, + "mangaTitle": "One Piece", + "chapterTitle": "Chapter 101", + "state": "Downloading", + "progress": 25, + "tries": 0 + }, + "downloadSpeed": 1024000 + } + ] +} +``` + +--- + +### `WS /api/v1/update` + +- **What it does:** Real-time library update progress +- **Expected request:** WebSocket connection with authentication + +#### Messages You Can Send to Server: + +**Request current update status:** +``` +STATUS +``` +- **Expected Response from Server:** +```json +{ + "status": "Running", + "pending": 0, + "running": true, + "mangaDone": 5, + "mangaTotal": 100, + "chapterDone": 50, + "chapterAdded": 10, + "progress": { + "current": 5, + "total": 100 + }, + "errors": [] +} +``` + +**Status values:** `Idle`, `Running`, `Completed`, `Stopped` + +**Invalid command response:** +``` +Invalid command. +Supported commands are: + - STATUS + sends the current update status +``` + +#### Messages Received from Server (Automatic Updates): + +The server automatically sends status updates during library updates. + +**Example update:** +```json +{ + "status": "Running", + "pending": 95, + "running": true, + "mangaDone": 5, + "mangaTotal": 100, + "chapterDone": 50, + "chapterAdded": 10, + "progress": { + "current": 5, + "total": 100 + }, + "errors": [] +} +``` + +--- + +### `WS /api/v1/webview` + +- **What it does:** Real-time webview communication for embedded browser functionality +- **Expected request:** WebSocket connection with authentication + +#### Messages You Can Send to Server: + +**Load a URL in the webview:** +```json +{ + "type": "loadUrl", + "url": "https://example.com", + "width": 1920, + "height": 1080 +} +``` +- **Expected Response:** None (webview loads the URL) + +**Resize the webview:** +```json +{ + "type": "resize", + "width": 800, + "height": 600 +} +``` +- **Expected Response:** None + +**Send a JavaScript event:** +```json +{ + "type": "event", + "eventType": "click", + "clickX": 100.5, + "clickY": 200.75, + "button": 0, + "ctrlKey": false, + "shiftKey": false, + "altKey": false, + "metaKey": false +} +``` +- **Expected Response:** None (event is sent to the webview) + +**Keyboard event:** +```json +{ + "type": "event", + "eventType": "keydown", + "key": "Enter", + "code": "Enter", + "ctrlKey": true +} +``` +- **Expected Response:** None + +**Scroll event:** +```json +{ + "type": "event", + "eventType": "wheel", + "deltaY": -100, + "clientX": 500, + "clientY": 500 +} +``` +- **Expected Response:** None + +**Paste text to webview:** +```json +{ + "type": "paste", + "data": "pasted text content" +} +``` +- **Expected Response:** None + +**Copy from webview:** +```json +{ + "type": "copy" +} +``` +- **Expected Response:** None + +**Ping to check connection:** +```json +{ + "type": "ping" +} +``` +- **Expected Response from Server:** +```json +{ + "type": "pong" +} +``` + +#### Messages Received from Server: + +The server sends various responses based on webview interactions: + +**Pong response:** +```json +{ + "type": "pong" +} +``` + +**JavaScript console output (example):** +```json +{ + "type": "console", + "level": "log", + "message": "Page loaded successfully" +} +``` + +**Page load events:** +```json +{ + "type": "loadStart", + "url": "https://example.com" +} +``` + +```json +{ + "type": "loadFinish", + "url": "https://example.com", + "httpStatusCode": 200 +} +``` + +**Error events:** +```json +{ + "type": "error", + "description": "Failed to load resource", + "url": "https://example.com/broken-link" +} +``` + +--- + +## GraphQL Endpoint + +**Base URL:** `http://localhost:4567/api/graphql` + +The server also provides a GraphQL API for more flexible queries and subscriptions. See the GraphQL schema for available queries, mutations, and subscriptions. + +--- + +## OPDS Endpoints + +The OPDS API provides catalog feeds for integration with OPDS-compatible readers. + +**Base URL:** `http://localhost:4567/api/opds/v1.2/` + +### `GET /api/opds/v1.2/opds/v1.2` + +- **What it does:** OPDS Catalog Root Feed (Navigation) +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +**Response XML Example:** +```xml + + + Suwayomi + + + + Library + + Your manga library + + + Explore + + Browse sources + + +``` + +### `GET /api/opds/v1.2/search` + +- **What it does:** OPDS Search Description Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/explore` + +- **What it does:** Explore Sources Navigation Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/history` + +- **What it does:** Reading History Acquisition Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/library-updates` + +- **What it does:** Library Updates Acquisition Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/library/series` + +- **What it does:** All Series in Library / Search Results Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/library/sources` + +- **What it does:** Library Sources Navigation Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/library/source/{sourceId}` + +- **What it does:** Library Source-Specific Series Acquisition Feed +- **Expected request:** Path parameter: `sourceId` (long) +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/library/categories` + +- **What it does:** Library Categories Navigation Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/library/genres` + +- **What it does:** Library Genres Navigation Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/library/statuses` + +- **What it does:** Library Status Navigation Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/library/languages` + +- **What it does:** Library Content Languages Navigation Feed +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/sources` + +- **What it does:** All Sources Navigation Feed (Explore) +- **Expected request:** No parameters required +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/source/{sourceId}` + +- **What it does:** Source-Specific Series Acquisition Feed (Explore) +- **Expected request:** Path parameter: `sourceId` (long) +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/series/{seriesId}/chapters` + +- **What it does:** Series Chapters Acquisition Feed +- **Expected request:** Path parameter: `seriesId` (int) +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/series/{seriesId}/chapter/{chapterIndex}/metadata` + +- **What it does:** Chapter Metadata Acquisition Feed +- **Expected request:** Path parameters: `seriesId` (int), `chapterIndex` (int) +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/category/{categoryId}` + +- **What it does:** Category-Specific Series Acquisition Feed +- **Expected request:** Path parameter: `categoryId` (int) +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/genre/{genre}` + +- **What it does:** Genre-Specific Series Acquisition Feed +- **Expected request:** Path parameter: `genre` (string) +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/status/{statusId}` + +- **What it does:** Status-Specific Series Acquisition Feed +- **Expected request:** Path parameter: `statusId` (int) +- **Expected response:** XML OPDS feed (HTTP 200) + +### `GET /api/opds/v1.2/language/{langCode}` + +- **What it does:** Language-Specific Series Acquisition Feed +- **Expected request:** Path parameter: `langCode` (string) +- **Expected response:** XML OPDS feed (HTTP 200) + +--- + +## Notes + +- The server runs on port 4567 by default +- All JSON responses use UTF-8 encoding +- File uploads use multipart/form-data format +- WebSocket endpoints provide real-time updates for downloads, updates, and webview +- WebSocket messages are JSON formatted (except STATUS command which is plain text) +- Image responses include caching headers with 1-day max-age for thumbnails and pages +- OPDS feeds use Atom XML format for catalog navigation and acquisition +- Actions like start/stop for downloads and updates are done via HTTP endpoints, not WebSocket +- WebSocket endpoints only support "STATUS" command to query current status +- See [Authentication](#authentication) section for details on how to authenticate with the API \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..3b56dfbc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,105 @@ +FROM eclipse-temurin:25.0.3_9-jdk-noble AS build + +ARG TACHIDESK_ABORT_HANDLER_DOWNLOAD_URL + +# build abort handler +RUN if [ -n "$TACHIDESK_ABORT_HANDLER_DOWNLOAD_URL" ]; then \ + apt-get update && \ + apt-get -y install -y curl gcc && \ + cd /tmp && \ + curl "$TACHIDESK_ABORT_HANDLER_DOWNLOAD_URL" -O && \ + gcc -fPIC -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -shared catch_abort.c -lpthread -o /opt/catch_abort.so && \ + rm -f catch_abort.c && \ + apt-get -y purge gcc --auto-remove && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* || exit 1; \ + fi + +# Build the server jar from source +WORKDIR /app +COPY . . +RUN GRADLE_OPTS="-Xmx4g" ./gradlew :server:shadowJar --no-daemon -x test + +FROM eclipse-temurin:25.0.3_9-jre-noble + +ARG TARGETPLATFORM +ARG TACHIDESK_KCEF=n # y or n, leave empty for auto-detection +ARG TACHIDESK_KCEF_RELEASE_URL + +# Install envsubst from GNU's gettext project +# install unzip to unzip the server-reference.conf from the jar +# Install tini for a tiny init system (handles orphan processes for graceful restart) +RUN apt-get update && \ + apt-get -y install -y curl gettext-base unzip tini ca-certificates p11-kit && \ + /usr/bin/p11-kit extract --format=java-cacerts --filter=certificates --overwrite --purpose server-auth $JAVA_HOME/lib/security/cacerts && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +COPY scripts/kcef_download.sh /root/kcef_download.sh +RUN chmod +x /root/kcef_download.sh + +# install CEF dependencies +RUN if [ "$TACHIDESK_KCEF" = "y" ] || ([ "$TACHIDESK_KCEF" = "" ] && ([ "$TARGETPLATFORM" = "linux/amd64" ] || [ "$TARGETPLATFORM" = "linux/arm64" ])); then \ + apt-get update && \ + apt-get -y install --no-install-recommends -y libxss1 libxext6 libxrender1 libxcomposite1 libxdamage1 libxkbcommon0 libxtst6 \ + libjogl2-jni libgluegen2-jni libglib2.0-0t64 libnss3 libdbus-1-3 libpango-1.0-0 libcairo2 libasound2t64 \ + libatk-bridge2.0-0t64 libcups2t64 libdrm2 libgbm1 xvfb \ + curl jq gawk findutils && \ + /root/kcef_download.sh "$TACHIDESK_KCEF_RELEASE_URL" "$TARGETPLATFORM" && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* || exit 1; \ + fi + +COPY --from=build /opt/*.so /opt/ + +# Create a user to run as +# .X11-unix must be created by root +# Ubuntu exposes libgluegen_rt.so as libgluegen2_rt.so for some reason, so rename it +# JCEF (or Java?) also does not search /usr/lib/jni, so copy them over into one it will search +RUN userdel -r ubuntu && \ + groupadd --gid 1000 suwayomi && \ + useradd --uid 1000 --gid suwayomi --no-log-init -G audio,video suwayomi && \ + mkdir -p /home/suwayomi/.local/share/Tachidesk && \ + if command -v Xvfb; then \ + mkdir /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix && \ + cp /usr/lib/jni/libgluegen2_rt.so /home/suwayomi/libgluegen_rt.so && \ + cp /usr/lib/jni/*.so /home/suwayomi/; \ + fi + +COPY scripts/create_server_conf.sh /home/suwayomi/create_server_conf.sh +COPY scripts/startup_script.sh /home/suwayomi/startup_script.sh +RUN chmod +x /home/suwayomi/create_server_conf.sh /home/suwayomi/startup_script.sh + +# Copy locally built jar; grant o+rwx so non-default UIDs can write server.conf +RUN mkdir -p /home/suwayomi/startup +COPY --from=build /app/server/build/*.jar /home/suwayomi/startup/tachidesk_latest.jar +RUN chmod 777 -R /home/suwayomi && \ + chown -R suwayomi:suwayomi /home/suwayomi + +ARG BUILD_DATE +ARG TACHIDESK_RELEASE_TAG +ARG TACHIDESK_FILENAME +ARG TACHIDESK_DOCKER_GIT_COMMIT +LABEL maintainer="suwayomi" \ + org.opencontainers.image.title="Suwayomi Docker" \ + org.opencontainers.image.authors="https://github.com/suwayomi" \ + org.opencontainers.image.url="https://github.com/suwayomi/docker-tachidesk/pkgs/container/tachidesk" \ + org.opencontainers.image.source="https://github.com/suwayomi/docker-tachidesk" \ + org.opencontainers.image.description="This image is used to start suwayomi server in a container" \ + org.opencontainers.image.vendor="suwayomi" \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.version=$TACHIDESK_RELEASE_TAG \ + tachidesk.docker_commit=$TACHIDESK_DOCKER_GIT_COMMIT \ + tachidesk.release_tag=$TACHIDESK_RELEASE_TAG \ + tachidesk.filename=$TACHIDESK_FILENAME \ + org.opencontainers.image.licenses="MPL-2.0" + +ENV HOME=/home/suwayomi +WORKDIR /home/suwayomi +USER suwayomi +EXPOSE 4567 + +ENTRYPOINT ["tini", "--"] +CMD ["/home/suwayomi/startup_script.sh"] + +# vim: set ft=dockerfile: \ No newline at end of file diff --git a/docker-compose-postgresql.yml b/docker-compose-postgresql.yml new file mode 100644 index 00000000..2ef62d05 --- /dev/null +++ b/docker-compose-postgresql.yml @@ -0,0 +1,71 @@ +--- +services: + suwayomi: + build: . + platform: linux/amd64 + image: registry.achmad.dev/suwayomi-server:latest + # user: 1000:1000 + environment: + - TZ=${TZ:-Etc/UTC} # Add a TZ variable to .env to change it + - DATABASE_TYPE=POSTGRESQL + - DATABASE_URL=postgresql://postgresql:5432/${POSTGRES_DB} + - DATABASE_USERNAME=${POSTGRES_USER} + - DATABASE_PASSWORD=${POSTGRES_PASSWORD} + # - USE_HIKARI_CONNECTION_POOL=false # Hikari Connection Pool can cause issues with some installations, but it is much more performant. + # Comment these out if you do not use the flaresolverr container at the bottom of this file + - FLARESOLVERR_ENABLED=true + - FLARESOLVERR_URL=http://flaresolverr:8191 +# ################################################################################################# +# +# !!! IMPORTANT !!! +# - server settings can be changed during runtime in the WebUI +# - providing an environment variable will OVERWRITE the current setting value when starting the container +# +# ################################################################################################# +# +# example for setting env vars: +# +# - BIND_IP=0.0.0.0 +# - BIND_PORT=4567 +# - SOCKS_PROXY_ENABLED=false +# - DOWNLOAD_AS_CBZ=true +# - AUTH_MODE=basic_auth +# - AUTH_USERNAME=manga +# - AUTH_PASSWORD=hello123 +# - EXTENSION_REPOS=["http://github.com/orginazation-name/repo-name", "http://github.com/orginazation-name-2/repo-name-2"] + depends_on: + postgresql: + condition: service_healthy + restart: true + volumes: + - ./data:/home/suwayomi/.local/share/Tachidesk + ports: + - "4567:4567" + restart: on-failure:3 + + postgresql: + image: postgres:18.3 + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - PGDATA=/data/postgres + - POSTGRES_DB=${POSTGRES_DB} + - TZ=${TZ:-Etc/UTC} + - PGTZ=${TZ:-Etc/UTC} + volumes: + - ./postgres:/data/postgres + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -d ${POSTGRES_DB} -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + flaresolverr: + image: ghcr.io/thephaseless/byparr:latest + container_name: flaresolverr + init: true + environment: + - TZ=${TZ:-Etc/UTC} + restart: unless-stopped \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..657bc631 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +--- +services: + suwayomi: + build: . + platform: linux/amd64 + image: registry.achmad.dev/suwayomi-server:latest + # user: 1000:1000 + environment: + - TZ=Etc/UTC # Use TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + # Comment these out if you do not use the flaresolverr container at the bottom of this file + - FLARESOLVERR_ENABLED=true + - FLARESOLVERR_URL=http://flaresolverr:8191 +# ################################################################################################# +# +# !!! IMPORTANT !!! +# - server settings can be changed during runtime in the WebUI +# - providing an environment variable will OVERWRITE the current setting value when starting the container +# +# ################################################################################################# +# +# example for setting env vars: +# +# - BIND_IP=0.0.0.0 +# - BIND_PORT=4567 +# - SOCKS_PROXY_ENABLED=false +# - DOWNLOAD_AS_CBZ=true +# - AUTH_MODE=basic_auth +# - AUTH_USERNAME=manga +# - AUTH_PASSWORD=hello123 +# - EXTENSION_REPOS=["http://github.com/orginazation-name/repo-name", "http://github.com/orginazation-name-2/repo-name-2"] + volumes: + - ./data:/home/suwayomi/.local/share/Tachidesk + ports: + - "4567:4567" + restart: on-failure:3 + + flaresolverr: + image: ghcr.io/thephaseless/byparr:latest + container_name: flaresolverr + init: true + environment: + - TZ=Etc/UTC # Use TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + restart: unless-stopped \ No newline at end of file diff --git a/scripts/create_server_conf.sh b/scripts/create_server_conf.sh new file mode 100644 index 00000000..dff16501 --- /dev/null +++ b/scripts/create_server_conf.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# exit early in case the file already exists +if [ -f /home/suwayomi/.local/share/Tachidesk/server.conf ]; then + exit 0 +fi + +mkdir -p /home/suwayomi/.local/share/Tachidesk + +# extract the server reference config from the jar +unzip -q -j /home/suwayomi/startup/tachidesk_latest.jar "server-reference.conf" -d /home/suwayomi/startup + +# move and rename the reference config +mv /home/suwayomi/startup/server-reference.conf /home/suwayomi/.local/share/Tachidesk/server.conf \ No newline at end of file diff --git a/scripts/kcef_download.sh b/scripts/kcef_download.sh new file mode 100644 index 00000000..0d9678f6 --- /dev/null +++ b/scripts/kcef_download.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +set -e + +url="$1" +arch="$2" +installdir="${3:-/opt/kcef/jcef}" + +echo "Will try to download matching KCEF to $installdir, arch=$arch, api url=$url" + +if [ -d "$installdir" ] && [ -f "$installdir/install.lock" ]; then + echo "JCEF already downloaded to $installdir, nothing to do" + exit 0 +fi + +if [ -z "$url" ]; then + echo "Not downloading KCEF since no URL specified" + exit 0 +fi + +arpath=/tmp/kcef/jcef.tar.gz +expath=/tmp/kcef/jcef +mkdir -p "$expath" + +if [ ! -f "$arpath" ]; then + body="`curl -# -H 'accept: application/vnd.github+json' "$url" | jq -r '.body'`" + archive="`echo "$body" | gawk -F'|' ' + function compare_sdk(i1, v1, i2, v2) { + # sort sdks last (https://github.com/DatL4g/KCEF/blob/0665269b7d6a91b0ee187f4432bb5be5ca41a112/kcef/src/main/kotlin/dev/datlag/kcef/KCEFBuilder.kt#L743-L755) + if (v1 ~ /sdk/ && v2 ~ /sdk/) return 0; + if (v1 ~ /sdk/) return 1; + if (v2 ~ /sdk/) return -1; + return 0; + } + BEGIN { + # ensure urls is an array + delete urls[0]; + # parse os/arch tuple + match("'"$arch"'", /(.*)\/(.*)/, a); + os=a[1]; + arch=a[2]; + if (arch == "amd64") arch="x64"; + if (arch == "arm64") arch="aarch64"; + } + # for each line, check that the third table column contains a url and if so, extract it + # also need to check that it contains JCEF and matches the os/arch tuple + match($4, /(https?:\/\/|www.)[-a-zA-Z0-9+&@#\/%?=~_|!:.;]*[-a-zA-Z0-9+&@#/%=~_|]/, m) { + # if so, push to the urls array; there is no push function, so do this cursed construction + # arrays by convention start at 1, so do that + if (m[0] ~ /jcef/ && m[0] ~ os && m[0] ~ arch && m[0] ~ /\.tar\.gz$/) urls[length(urls)+1] = m[0]; + } + END { + # now make sure sdk is sorted last, since we dont actually need the full sdk + asort(urls, sorted, "compare_sdk"); + for (x in sorted) print sorted[x]; + } + ' | head -n1`" + + if [ -z "$archive" ]; then + echo "No suitable archive found on release page, so not downloading" + exit 0 + fi + + echo "Found suitable JCEF release: $archive" + curl -# -L -H 'accept: application/x-tar' -o "$arpath" "$archive" +fi + +set -xe +tar -C "$expath" -xf "$arpath" +libfolder="`find "$expath" -type d -name lib`" + +if [ -z "$libfolder" ]; then + echo "lib folder not found in extracted archive, aborting" + rm -rf /tmp/kcef + exit 0 +fi + +mkdir -p "$installdir" +rmdir "$installdir" # we abuse -p to make sure all parent directories are created, then delete the actual target, since mv would move the libfolder inside otherwise +mv "$libfolder" "$installdir" +chmod -R a+x "$installdir" +touch "$installdir/install.lock" +rm -rf /tmp/kcef \ No newline at end of file diff --git a/scripts/startup_script.sh b/scripts/startup_script.sh new file mode 100644 index 00000000..5692a265 --- /dev/null +++ b/scripts/startup_script.sh @@ -0,0 +1,178 @@ +#!/bin/sh + +# Immediately bail out if any command fails: +set -e + +echo "Suwayomi data location inside the container: /home/suwayomi/.local/share/Tachidesk" + +# make sure the server.conf file exists +/home/suwayomi/create_server_conf.sh + +# set default values for environment variables: +export TZ="${TZ:-Etc/UTC}" + +# Getting passwords from files +if [ -f "${SOCKS_PROXY_PASSWORD_FILE}" ]; then + export SOCKS_PROXY_PASSWORD=$(cat "${SOCKS_PROXY_PASSWORD_FILE}") +fi +if [ -f "${AUTH_PASSWORD_FILE}" ]; then + export AUTH_PASSWORD=$(cat "${AUTH_PASSWORD_FILE}") +fi +if [ -f "${BASIC_AUTH_PASSWORD_FILE}" ]; then + export BASIC_AUTH_PASSWORD=$(cat "${BASIC_AUTH_PASSWORD_FILE}") +fi +if [ -f "${DATABASE_PASSWORD_FILE}" ]; then + export DATABASE_PASSWORD=$(cat "${DATABSE_PASSWORD_FILE}") +fi + + +# Set default values for settings +sed -i -r "s/server.initialOpenInBrowserEnabled = ([0-9]+|[a-zA-Z]+)( #)?/server.initialOpenInBrowserEnabled = false #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.systemTrayEnabled = ([0-9]+|[a-zA-Z]+)( #)?/server.systemTrayEnabled = false #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# !!! IMPORTANT: make sure to add new env variables to the container.yml workflow step testing the container with providing environment variables + +# Overwrite configuration values with environment variables +# the "( #)?" at the end of the regex prevents the settings comment from getting removed +# some settings might not have a comment, however, "sed" does not support non matching groups in a regex, thus, an empty +# comment will just be created for these settings + +# Server ip and port bindings +sed -i -r "s/server.ip = \"(.*?)\"( #)?/server.ip = \"${BIND_IP:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.port = ([0-9]+|[a-zA-Z]+)( #)?/server.port = ${BIND_PORT:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# Socks5 proxy +sed -i -r "s/server.socksProxyEnabled = ([0-9]+|[a-zA-Z]+)( #)?/server.socksProxyEnabled = ${SOCKS_PROXY_ENABLED:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.socksProxyVersion = ([0-9]+|[a-zA-Z]+)( #)?/server.socksProxyVersion = ${SOCKS_PROXY_VERSION:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.socksProxyHost = \"(.*?)\"( #)?/server.socksProxyHost = \"${SOCKS_PROXY_HOST:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.socksProxyPort = \"(.*?)\"( #)?/server.socksProxyPort = \"${SOCKS_PROXY_PORT:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.socksProxyUsername = \"(.*?)\"( #)?/server.socksProxyUsername = \"${SOCKS_PROXY_USERNAME:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.socksProxyPassword = \"(.*?)\"( #)?/server.socksProxyPassword = \"${SOCKS_PROXY_PASSWORD:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# webUI +sed -i -r "s/server.webUIEnabled = ([0-9]+|[a-zA-Z]+)( #)?/server.webUIEnabled = ${WEB_UI_ENABLED:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.webUIFlavor = \"*([a-zA-Z0-9_]+)\"*( #)?/server.webUIFlavor = ${WEB_UI_FLAVOR:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.webUIChannel = \"*([a-zA-Z0-9_]+)\"*( #)?/server.webUIChannel = ${WEB_UI_CHANNEL:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.webUIUpdateCheckInterval = ([0-9]+|[a-zA-Z]+)( #)?/server.webUIUpdateCheckInterval = ${WEB_UI_UPDATE_INTERVAL:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# downloader +sed -i -r "s/server.downloadAsCbz = ([0-9]+|[a-zA-Z]+)( #)?/server.downloadAsCbz = ${DOWNLOAD_AS_CBZ:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoDownloadNewChapters = ([0-9]+|[a-zA-Z]+)( #)?/server.autoDownloadNewChapters = ${AUTO_DOWNLOAD_CHAPTERS:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.excludeEntryWithUnreadChapters = ([0-9]+|[a-zA-Z]+)( #)?/server.excludeEntryWithUnreadChapters = ${AUTO_DOWNLOAD_EXCLUDE_UNREAD:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoDownloadNewChaptersLimit = ([0-9]+|[a-zA-Z]+)( #)?/server.autoDownloadNewChaptersLimit = ${AUTO_DOWNLOAD_NEW_CHAPTERS_LIMIT:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoDownloadIgnoreReUploads = ([0-9]+|[a-zA-Z]+)( #)?/server.autoDownloadIgnoreReUploads = ${AUTO_DOWNLOAD_IGNORE_REUPLOADS:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +if [ -n "$DOWNLOAD_CONVERSIONS" ]; then + perl -0777 -i -pe 's/server\.downloadConversions = ({[^#]*?}}?)/server.downloadConversions = $ENV{DOWNLOAD_CONVERSIONS}/gs' /home/suwayomi/.local/share/Tachidesk/server.conf +fi +if [ -n "$SERVE_CONVERSIONS" ]; then + perl -0777 -i -pe 's/server\.serveConversions = ({[^#]*?}}?)/server.serveConversions = $ENV{SERVE_CONVERSIONS}/gs' /home/suwayomi/.local/share/Tachidesk/server.conf +fi + +# extension repos +if [ -n "$EXTENSION_REPOS" ]; then + perl -0777 -i -pe 's/server\.extensionRepos = (\[.*?\])/server.extensionRepos = $ENV{EXTENSION_REPOS}/gs' /home/suwayomi/.local/share/Tachidesk/server.conf +fi + +# requests +sed -i -r "s/server.maxSourcesInParallel = ([0-9]+|[a-zA-Z]+)( #)?/server.maxSourcesInParallel = ${MAX_SOURCES_IN_PARALLEL:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# updater +sed -i -r "s/server.excludeUnreadChapters = ([0-9]+|[a-zA-Z]+)( #)?/server.excludeUnreadChapters = ${UPDATE_EXCLUDE_UNREAD:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.excludeNotStarted = ([0-9]+|[a-zA-Z]+)( #)?/server.excludeNotStarted = ${UPDATE_EXCLUDE_STARTED:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.excludeCompleted = ([0-9]+|[a-zA-Z]+)( #)?/server.excludeCompleted = ${UPDATE_EXCLUDE_COMPLETED:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.globalUpdateInterval = ([0-9\.]+|[a-zA-Z]+)( #)?/server.globalUpdateInterval = ${UPDATE_INTERVAL:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.updateMangas = ([0-9]+|[a-zA-Z]+)( #)?/server.updateMangas = ${UPDATE_MANGA_INFO:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# Authentication +AUTH_MODE_VAL="${AUTH_MODE:-$( [ "$BASIC_AUTH_ENABLED" = "true" ] && echo 'basic_auth' || echo "" )}" +AUTH_USERNAME_VAL="${AUTH_USERNAME:-$BASIC_AUTH_USERNAME}" +AUTH_PASSWORD_VAL="${AUTH_PASSWORD:-$BASIC_AUTH_PASSWORD}" +sed -i -r "s/server.authMode = \"*([a-zA-Z0-9_]+)\"*( #)?/server.authMode = ${AUTH_MODE_VAL:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.authUsername = \"(.*?)\"( #)?/server.authUsername = \"${AUTH_USERNAME_VAL:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.authPassword = \"(.*?)\"( #)?/server.authPassword = \"${AUTH_PASSWORD_VAL:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.jwtAudience = \"(.*?)\"( #)?/server.jwtAudience = \"${JWT_AUDIENCE:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.jwtTokenExpiry = \"(.*?)\"( #)?/server.jwtTokenExpiry = \"${JWT_TOKEN_EXPIRY:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.jwtRefreshExpiry = \"(.*?)\"( #)?/server.jwtRefreshExpiry = \"${JWT_REFRESH_EXPIRY:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +sed -i -r "s/server.basicAuthEnabled = ([0-9]+|[a-zA-Z]+)( #)?/server.basicAuthEnabled = ${BASIC_AUTH_ENABLED:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.basicAuthUsername = \"(.*?)\"( #)?/server.basicAuthUsername = \"${BASIC_AUTH_USERNAME:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.basicAuthPassword = \"(.*?)\"( #)?/server.basicAuthPassword = \"${BASIC_AUTH_PASSWORD:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# misc +sed -i -r "s/server.debugLogsEnabled = ([0-9]+|[a-zA-Z]+)( #)?/server.debugLogsEnabled = ${DEBUG:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.maxLogFiles = ([0-9]+|[a-zA-Z]+)( #)?/server.maxLogFiles = ${MAX_LOG_FILES:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.maxLogFileSize = \"(.*?)\"( #)?/server.maxLogFileSize = \"${MAX_LOG_FILE_SIZE:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.maxLogFolderSize = \"(.*?)\"( #)?/server.maxLogFolderSize = \"${MAX_LOG_FOLDER_SIZE:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# backup +sed -i -r "s/server.backupTime = \"(.*?)\"( #)?/server.backupTime = \"${BACKUP_TIME:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.backupInterval = ([0-9]+|[a-zA-Z]+)( #)?/server.backupInterval = ${BACKUP_INTERVAL:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.backupTTL = ([0-9]+|[a-zA-Z]+)( #)?/server.backupTTL = ${BACKUP_TTL:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoBackupIncludeManga = ([0-9]+|[a-zA-Z]+)( #)?/server.autoBackupIncludeManga = ${AUTO_BACKUP_INCLUDE_MANGA:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoBackupIncludeCategories = ([0-9]+|[a-zA-Z]+)( #)?/server.autoBackupIncludeCategories = ${AUTO_BACKUP_INCLUDE_CATEGORIES:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoBackupIncludeChapters = ([0-9]+|[a-zA-Z]+)( #)?/server.autoBackupIncludeChapters = ${AUTO_BACKUP_INCLUDE_CHAPTERS:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoBackupIncludeTracking = ([0-9]+|[a-zA-Z]+)( #)?/server.autoBackupIncludeTracking = ${AUTO_BACKUP_INCLUDE_TRACKING:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoBackupIncludeHistory = ([0-9]+|[a-zA-Z]+)( #)?/server.autoBackupIncludeHistory = ${AUTO_BACKUP_INCLUDE_HISTORY:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoBackupIncludeClientData = ([0-9]+|[a-zA-Z]+)( #)?/server.autoBackupIncludeClientData = ${AUTO_BACKUP_INCLUDE_CLIENT_DATA:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.autoBackupIncludeServerSettings = ([0-9]+|[a-zA-Z]+)( #)?/server.autoBackupIncludeServerSettings = ${AUTO_BACKUP_INCLUDE_SERVER_SETTINGS:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf + + +# cloudflare bypass +sed -i -r "s/server.flareSolverrEnabled = ([0-9]+|[a-zA-Z]+)( #)?/server.flareSolverrEnabled = ${FLARESOLVERR_ENABLED:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s|server.flareSolverrUrl = \"(.*?)\"( #)?|server.flareSolverrUrl = \"${FLARESOLVERR_URL:-\1}\" #|" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.flareSolverrTimeout = ([0-9]+|[a-zA-Z]+)( #)?/server.flareSolverrTimeout = ${FLARESOLVERR_TIMEOUT:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.flareSolverrSessionName = \"(.*?)\"( #)?/server.flareSolverrSessionName = \"${FLARESOLVERR_SESSION_NAME:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.flareSolverrSessionTtl = ([0-9]+|[a-zA-Z]+)( #)?/server.flareSolverrSessionTtl = ${FLARESOLVERR_SESSION_TTL:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.flareSolverrAsResponseFallback = ([0-9]+|[a-zA-Z]+)( #)?/server.flareSolverrAsResponseFallback = ${FLARESOLVERR_RESPONSE_AS_FALLBACK:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# opds +sed -i -r "s/server.opdsUseBinaryFileSizes = ([0-9]+|[a-zA-Z]+)( #)?/server.opdsUseBinaryFileSizes = ${OPDS_USE_BINARY_FILE_SIZES:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.opdsItemsPerPage = ([0-9]+|[a-zA-Z]+)( #)?/server.opdsItemsPerPage = ${OPDS_ITEMS_PER_PAGE:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.opdsEnablePageReadProgress = ([0-9]+|[a-zA-Z]+)( #)?/server.opdsEnablePageReadProgress = ${OPDS_ENABLE_PAGE_READ_PROGRESS:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.opdsMarkAsReadOnDownload = ([0-9]+|[a-zA-Z]+)( #)?/server.opdsMarkAsReadOnDownload = ${OPDS_MARK_AS_READ_ON_DOWNLOAD:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.opdsShowOnlyUnreadChapters = ([0-9]+|[a-zA-Z]+)( #)?/server.opdsShowOnlyUnreadChapters = ${OPDS_SHOW_ONLY_UNREAD_CHAPTERS:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.opdsShowOnlyDownloadedChapters = ([0-9]+|[a-zA-Z]+)( #)?/server.opdsShowOnlyDownloadedChapters = ${OPDS_SHOW_ONLY_DOWNLOADED_CHAPTERS:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.opdsChapterSortOrder = \"*([a-zA-Z0-9_]+)\"*( #)?/server.opdsChapterSortOrder = ${OPDS_CHAPTER_SORT_ORDER:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.opdsCbzMimetype = \"*([a-zA-Z0-9_]+)\"*( #)?/server.opdsCbzMimetype = ${OPDS_CBZ_MIME_TYPE:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# koreader +sed -i -r "s/server.koreaderSyncChecksumMethod = \"*([a-zA-Z0-9_]+)\"*( #)?/server.koreaderSyncChecksumMethod = ${KOREADER_SYNC_CHECKSUM_METHOD:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.koreaderSyncPercentageTolerance = ([-0-9\.Ee]+)?( #)/server.koreaderSyncPercentageTolerance = ${KOREADER_SYNC_PERCENTAGE_TOLERANCE:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.koreaderSyncStrategyForward = \"*([a-zA-Z0-9_]+)\"*( #)?/server.koreaderSyncStrategyForward = ${KOREADER_SYNC_STRATEGY_FORWARD:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.koreaderSyncStrategyBackward = \"*([a-zA-Z0-9_]+)\"*( #)?/server.koreaderSyncStrategyBackward = ${KOREADER_SYNC_STRATEGY_BACKWARD:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +# database +sed -i -r "s/server.databaseType = \"*([a-zA-Z0-9_]+)\"*( #)?/server.databaseType = ${DATABASE_TYPE:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s|server.databaseUrl = \"(.*?)\"( #)?|server.databaseUrl = \"${DATABASE_URL:-\1}\" #|" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.databaseUsername = \"(.*?)\"( #)?/server.databaseUsername = \"${DATABASE_USERNAME:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.databasePassword = \"(.*?)\"( #)?/server.databasePassword = \"${DATABASE_PASSWORD:-\1}\" #/" /home/suwayomi/.local/share/Tachidesk/server.conf +sed -i -r "s/server.useHikariConnectionPool = ([0-9]+|[a-zA-Z]+)( #)?/server.useHikariConnectionPool = ${USE_HIKARI_CONNECTION_POOL:-\1} #/" /home/suwayomi/.local/share/Tachidesk/server.conf + +rm -rf /home/suwayomi/.local/share/Tachidesk/cache/kcef/Singleton* + +if command -v Xvfb >/dev/null; then + command="xvfb-run --auto-servernum java" + if [ -d /opt/kcef/jcef ]; then + # if we have KCEF downloaded in the container, attempt to link it into the data directory where Suwayomi expects it + if [ ! -d /home/suwayomi/.local/share/Tachidesk/bin ]; then + mkdir -p /home/suwayomi/.local/share/Tachidesk/bin + fi + if [ ! -d /home/suwayomi/.local/share/Tachidesk/bin/kcef ] && [ ! -L /home/suwayomi/.local/share/Tachidesk/bin/kcef ]; then + ln -s /opt/kcef/jcef /home/suwayomi/.local/share/Tachidesk/bin/kcef + fi + fi + if [ -d /home/suwayomi/.local/share/Tachidesk/bin/kcef ] || [ -L /home/suwayomi/.local/share/Tachidesk/bin/kcef ]; then + # make sure all files are always executable. KCEF (and our downloader) ensure this on creation, but if the flag is lost + # at some point, CEF will die + chmod -R a+x /home/suwayomi/.local/share/Tachidesk/bin/kcef 2>/dev/null || true + fi + export LD_PRELOAD=/home/suwayomi/.local/share/Tachidesk/bin/kcef/libcef.so +else + command="java" + echo "Suwayomi built without KCEF support, not starting Xvfb" +fi +if [ -f /opt/catch_abort.so ]; then + export LD_PRELOAD="/opt/catch_abort.so $LD_PRELOAD" +fi +echo "LD_PRELOAD=$LD_PRELOAD" +exec $command -Duser.home=/home/suwayomi -jar "/home/suwayomi/startup/tachidesk_latest.jar"; \ No newline at end of file diff --git a/scripts/tachidesk_version.json b/scripts/tachidesk_version.json new file mode 100644 index 00000000..301d593f --- /dev/null +++ b/scripts/tachidesk_version.json @@ -0,0 +1,3 @@ +{ + "stable": "v2.2.2100" +} \ No newline at end of file diff --git a/suwayomi-server.container b/suwayomi-server.container new file mode 100644 index 00000000..6274479f --- /dev/null +++ b/suwayomi-server.container @@ -0,0 +1,26 @@ +# Rootless quadlet files are placed in $HOME/.config/containers/systemd + +[Container] +Environment=TZ=Etc/UTC # Use TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +Image=ghcr.io/suwayomi/suwayomi-server:preview +PublishPort=4567:4567 + +UserNS=keep-id +# Without UserNS=keep-id, the container's UID 1000 would map to a diffferent host UID which probably doesn't own the bind-mounted directory below + +Volume=/path/to/downloads:/home/suwayomi/.local/share/Tachidesk/downloads:Z +# Podman will not create the directory automatically so make sure to create it beforehand +# On systems with SELinux, add the :Z flag otherwise SELinux will block the container from accessing the directory + +Volume=suwayomi-data.volume:/home/suwayomi/.local/share/Tachidesk +# Named volumes should be defined in a .volume quadlet otherwise Podman will assign the root of the volume to container UID 0 +# $ cat $HOME/.config/containers/systemd/suwayomi-data.volume +# [Volume] +# VolumeName=suwayomi-data + +[Install] +WantedBy=default.target +# Sets Suwayomi to start on boot + +[Service] +Restart=on-failure \ No newline at end of file