就像影视剧、音乐一样,漫画也是可以罗列的资源。

看着成百上千册的漫画匹配上完整的元数据,整整齐齐地分门别类,往那一摆,就像是艺术品一样。这种成就感甭提有多享受了!

但是,影音有着 Emby/Jellyfin/Plex 等多种成熟的解决方案,漫画却没有。目前我发现比较容易上手的漫画管理程序,大概就只有 Komga

关于

Komga 是一款免费且开源的漫画媒体服务器软件。支持常见的 CBZCBRPDFEPUB 格式的漫画或文件,可以直接在网页端阅读、编辑漫画的各类元数据。在移动端方面,可以使用官方的 Tachiyomi 扩展插件,任何支持 OPDS 协议的阅读 App。同时,Komga 内置账户系统,支持第三方渠道(Google、Github 和 Facebook)登录,可以任意创建账户,并为不同的账户分配阅读权限。另外,作者还在着手推广和完善漫画的元数据规范 ComicInfo.xml。可以猜想,未来可能会有专门的漫画元数据生成软件出现。

具体的使用示例请看下文演示。

部署

在 NAS 上部署

以群晖 DSM 7.1 系统为例,介绍使用 Docker 套件安装 Komga 的具体步骤。

安装 Docker 套件

参见此处教程:传送门

安装 Komga

在「注册表」中搜索「gotson/komga」或「komga」,下载最新的 latest 版本。

目前官方已支持简体中文,无需下载久未更新的汉化版本。

待镜像下载完毕后,在「映像」列表找到该镜像,双击以安装。

网络设置建议选择「bridge」,也可以选择「使用与 Docker Host 相同的网络」,这样会直接使用默认的 8080 端口。但是 8080 又是常用端口,所以还是选 bridge 后自定义一个好。

点击下一步。「容器名称」自由发挥,勾选「启用自动重新启动」。

点击「高级设置」,点击左上角「新增」,增加一项时区变量。

在左侧「变量」中填入 TZ,右侧「值」中填入 Asia/Shanghai,解决可能出现的容器内时区与本地不一致的问题。点击「保存」。

点击下一步。在端口设置中,左侧「本地端口」可以自由发挥,比如 2333,右侧「容器端口」固定不动。

点击下一步。在存储空间设置中,点击添加文件夹。

/docker 文件夹中新增一个 komga 文件夹,右侧「装载路径」填入 /config。此为 Komga 数据文件存放路径。

选择本地存放漫画的路径,比如我的是 /media/book,右侧「装载路径」可以自由发挥,比如 /book/comic

点击下一步、完成,启动 Komga。

在 Linux 上部署

安装 Docker

参考此处教程:传送门

安装 Komga

比如,我直接在 root 目录安装,并将 Komga 放在 myapp 文件夹中,在其中创建一个 Komga 文件夹用以存放程序数据,漫画则直接放置在 /root/comic 中。接下来,只要创建一个 docker-compose.yml 文件,填入以下内容:

version: "3.9"
services:

  Komga:
    image: gotson/komga:latest
    container_name: komga
    ports:
      - 2333:8080 #左侧2333可以任意修改
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - ./komga:/config
      - /root/comic:/comic
   #  - /book:/book
    restart: unless-stopped

使用 docker compose up -d 启动容器,Komga 就能顺利启动。使用 docker compose down 停止容器。

大概的目录树结构如下,你可以按喜好自行更改:


root
├── comic
└── myapp
    ├── docker-compose.yml
    └── komga

在 Windows 上部署

直接运行

Komga 是一个 Java 程序,可以直接在 Java 8+ 或 1.8+ 的环境中运行。

因此官方的安装建议是在 Github Releases 页面下载.jar文件,使用以下命令运行(正式运行时需将 x.y.z 替换为具体版本号):

java -jar komga-x.y.z.jar

当然首先你需要安装 Java 环境,或者使用 java -version 检测是否已安装 Java,如:

PS C:\Users\admin> java -version
java version "18.0.1.1" 2022-04-22
Java(TM) SE Runtime Environment (build 18.0.1.1+2-6)
Java HotSpot(TM) 64-Bit Server VM (build 18.0.1.1+2-6, mixed mode, sharing)

随后下载 Komga 程序文件,执行运行命令,例如:

java -jar komga-0.157.4.jar

控制台会跑一堆代码,并弹出 Java 的网络防火墙窗口。

默认的数据存放路径在%USERPROFILE%\.komga

使用 javaw 替代 java 命令可以实现后台无终端运行:

javaw -jar komga-0.157.4.jar

只是我不会使用 Java 控制程序,官方教程也没有提供关闭程序的命令。更何况我还不想去查。

似乎可以直接杀死 Java 进程来结束 Komga……

但我觉得运行 .jar 程序的方法不便更新,所以又试了试使用包管理器 scoop 安装 Komga 的方法。

使用 Scoop 安装

安装 Scoop:

iwr -useb get.scoop.sh | iex

具体步骤请参考 官方教程 或是其他中文类教程,这里跳过。

添加软件源:

scoop bucket add java extras

安装 Java,已经安装过 Java 的话就不用安了:

scoop install java/temurin-lts-jdk

安装 Komga:

scoop install komga

安装的大致过程如下:

PS C:\Users\admin> scoop install komga
Updating Scoop...
Updating 'extras' bucket...
Installing 'komga' (0.157.4) [64bit] from extras bucket
komga-0.157.4.jar (132.2 MB) [================================================================================] 100%
Checking hash of komga-0.157.4.jar ... ok.
Linking ~\Scoop\apps\komga\current => ~\Scoop\apps\komga\0.157.4
Creating shim for 'komga'.
Persisting config
'komga' (0.157.4) was installed successfully!
Notes
-----
Default URL is http://localhost:8080
'komga' suggests installing 'java/oraclejdk' or 'java/openjdk'.

启动 Komga:

komga

默认的数据存放路径在%USERPROFILE%\scoop\apps\komga\current\config

更新 Komga:

scoop update komga

卸载 Komga:

scoop uninstall komga

同样地,我也不知道怎么持续化后台进程……

所以我的建议是在电脑上安个 Docker 跑容器,方便控制……

反向代理

如果需要绑定域名,可以使用 Nginx 反向代理 Komga。

server 配置中添加一段:

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://your_server_url:8080;
        proxy_http_version 1.1;
    }

如果是使用 Caddy,可以参考 官方文档 关于 HTTPS 部分的配置。

配置

初始化

访问 ip:port 到达 Komga 的 web 界面。例如我这边访问 127.0.0.1:2333 就行。

Komga 的语言默认是英文,目前并没有找到固定初始语言的配置。

所以在左下角点击 Translation 切换至 简体中文

更改语言
更改语言

再按照说明创建「用户账户」。

创建账户
创建账户

一切就绪后,Komga 的主界面就展现在眼前了。

创建漫画库

点击左侧「库」旁边的 + 按钮,创建一个漫画库。

  • 随便填一个你喜欢的名字,比如「漫画」
  • 点击「浏览」按钮,找到你存放漫画的位置。容器的话填写的是容器内部的路径,例如上文教程中的/comic
  • 他这「下一本」应该是翻译错了,是「下一步」才对……

  • 可以勾选:

    • 每次扫描后自动清空垃圾箱
    • 自动转换为CBZ格式
  • 其他的选项自行评估其作用

「元数据」保持默认就行,点击「添加」按钮就完事了。

编辑元数据

一部元数据完善的漫画,大概包括以下信息:

  • 标题
  • 作者
  • 连载情况
  • 语言
  • 阅读方向
  • 简介
  • 出版社
  • 流派
  • 标签
  • ……

就像这样
就像这样

如果漫画文件中存在 ComicInfo.xml 元数据文件,那么会被 Komga 直接识别,就无需手动补齐元数据了。但是一般情况下是没有的,所以需要手动补充。

例如,直接在系列页面的右上角,点击编辑按钮,就可以补充除了作者以外的全部信息。

元数据编辑
元数据编辑

而漫画作者,则需要在单册漫画中编辑。你可以详尽地补充漫画的作者、铅稿、画图者、上色者、嵌字者、封面作者、主编乃至翻译者等等事无巨细的信息。

细心的朋友可能会注意到我的右上角图标多了两个,这是油猴脚本 KoMI 的功劳。

但是这个脚本只支持英文元数据,如果有大佬根据此脚本的原理利用上国内的漫画网站,或是 番组计划 上夹生的漫画介绍,对于元数据补充这一块会是个不小的帮助。

导入漫画

Komga 支持导入漫画,但是,并不是直接从网页上传漫画至 Komga,而是指从存储库中导入漫画,新增替换现存的漫画。

官方是这么说的:

此屏幕允许您导入现有库之外的文件. 只能将文件导入到现有序列, 在这种情况下Komga会将文件移动或复制到所选序列的目录中.

如果您为一本书选择了一个编号, 并且已经存在一本带有该编号的书,你可以比较这两本书.如果您决定导入该书. Komga将升级现有的书使用新文件, 有效地用新文件替换旧文件.

我觉得理解起来还是挺费劲的,需要具体操作几次才能明白。

大概测试了下,假如你的文件结构是这样的:

comic
├── test
|   ├── 漫画1
|   └── 漫画2
└── test2
    ├── 漫画3
    └── 质量更好的漫画2
  • 你已将 /comic/test 设置为一个漫画库
  • 名为 test 的文件夹被自动识别为一个漫画系列
  • test2 中的 漫画3 新增到 test 系列
  • test2 中的 质量更好的漫画2 替换 test 中的 漫画2

无论是新增或是替换,都需要确定被变更的漫画系列,并确定编号。

如果是新增,将编号设置为当前系列未被占用的编号即可。

如果是替换,可以点击 漫画详情 或 图像预览 按钮进行对比。

我测试的时候,大概是漫画文件在 /root/comic/ 内,权限不足,一直没能移动成功。假如你也遇到了这种情况,在环境变量中增加两项即可:

前面省略
environment:
  - TZ=Asia/Shanghai
  - PUID=0
  - PGID=0
后面省略

成功导入会有提示,如果没有提示,那一定是没有文件访问权限。

分配权限

假如有某些漫画不想让 Komga 的其他用户看到,你可以在点击「服务器设置」,选择特定用户修改他的访问权限。

权限分配
权限分配

整理

按系列创建文件夹

在使用过程中发现,如果漫画文件中没有内嵌ComicInfo.xml文件,Komga 会根据文件夹判断漫画系列。

假设你的漫画都存放在/comic文件夹中,并按照系列建立了二级文件夹,又在二级文件夹中为漫画带上数字序号,例如:

comic
├── 火影
│   ├── 火影 1
│   ├── 火影 2
│   └── ……
├── 海贼
│   ├── 海贼 1
│   ├── 海贼 2
│   └── ……
└── 灌篮
    ├── 灌篮 1
    ├── 灌篮 2
    └── ……

那么会得到这样的展示效果:

每个系列中的漫画是这样的:

但是,如果你的漫画非常乱,不仅没有按系列分类,还不带卷数数字序号,直接一股脑都丢进了 /comic 文件夹里。那么场面会相当混乱,如:

所以在添加漫画之前,你至少需要按系列创建文件夹,整理好你的漫画资源才行。

内嵌元数据

在一开始我提到过,Komga 支持识别内嵌在 CBR/CBZ 文件中的 ComicInfo.xml 数据。

这是一种始于 ComicRack 这种古早程序的元数据标准,现已经年久失修。所以作者联合有志之士一起创建了一个用以修订 ComicInfo.xml 的项目:The Anansi Project

一份完整的 2.0 版本的 ComicInfo.xml 长这样:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="ComicInfo" nillable="true" type="ComicInfo" />
    <xs:complexType name="ComicInfo">
        <xs:sequence>
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Title" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Series" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Number" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="-1" name="Count" type="xs:int" />
            <xs:element minOccurs="0" maxOccurs="1" default="-1" name="Volume" type="xs:int" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateSeries" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateNumber" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="-1" name="AlternateCount" type="xs:int" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Summary" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Notes" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="-1" name="Year" type="xs:int" />
            <xs:element minOccurs="0" maxOccurs="1" default="-1" name="Month" type="xs:int" />
            <xs:element minOccurs="0" maxOccurs="1" default="-1" name="Day" type="xs:int" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Writer" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Penciller" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Inker" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Colorist" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Letterer" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="CoverArtist" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Editor" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Publisher" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Imprint" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Genre" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Web" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="0" name="PageCount" type="xs:int" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="LanguageISO" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Format" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="BlackAndWhite" type="YesNo" />
            <xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="Manga" type="Manga" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Characters" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Teams" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Locations" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="ScanInformation" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="StoryArc" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="SeriesGroup" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="AgeRating" type="AgeRating" />
            <xs:element minOccurs="0" maxOccurs="1" name="Pages" type="ArrayOfComicPageInfo" />
            <xs:element minOccurs="0" maxOccurs="1" name="CommunityRating" type="Rating" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="MainCharacterOrTeam" type="xs:string" />
            <xs:element minOccurs="0" maxOccurs="1" default="" name="Review" type="xs:string" />
        </xs:sequence>
    </xs:complexType>
    <xs:simpleType name="YesNo">
        <xs:restriction base="xs:string">
            <xs:enumeration value="Unknown" />
            <xs:enumeration value="No" />
            <xs:enumeration value="Yes" />
        </xs:restriction>
    </xs:simpleType>
    <xs:simpleType name="Manga">
        <xs:restriction base="xs:string">
            <xs:enumeration value="Unknown" />
            <xs:enumeration value="No" />
            <xs:enumeration value="Yes" />
            <xs:enumeration value="YesAndRightToLeft" />
        </xs:restriction>
    </xs:simpleType>
    <xs:simpleType name="Rating">
        <xs:restriction base="xs:decimal">
            <xs:minInclusive value="0"/>
            <xs:maxInclusive value="5"/>
            <xs:fractionDigits value="2"/>
        </xs:restriction>
    </xs:simpleType>
    <xs:simpleType name="AgeRating">
        <xs:restriction base="xs:string">
            <xs:enumeration value="Unknown" />
            <xs:enumeration value="Adults Only 18+" />
            <xs:enumeration value="Early Childhood" />
            <xs:enumeration value="Everyone" />
            <xs:enumeration value="Everyone 10+" />
            <xs:enumeration value="G" />
            <xs:enumeration value="Kids to Adults" />
            <xs:enumeration value="M" />
            <xs:enumeration value="MA15+" />
            <xs:enumeration value="Mature 17+" />
            <xs:enumeration value="PG" />
            <xs:enumeration value="R18+" />
            <xs:enumeration value="Rating Pending" />
            <xs:enumeration value="Teen" />
            <xs:enumeration value="X18+" />
        </xs:restriction>
    </xs:simpleType>
    <xs:complexType name="ArrayOfComicPageInfo">
        <xs:sequence>
            <xs:element minOccurs="0" maxOccurs="unbounded" name="Page" nillable="true" type="ComicPageInfo" />
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="ComicPageInfo">
        <xs:attribute name="Image" type="xs:int" use="required" />
        <xs:attribute default="Story" name="Type" type="ComicPageType" />
        <xs:attribute default="false" name="DoublePage" type="xs:boolean" />
        <xs:attribute default="0" name="ImageSize" type="xs:long" />
        <xs:attribute default="" name="Key" type="xs:string" />
        <xs:attribute default="" name="Bookmark" type="xs:string" />
        <xs:attribute default="-1" name="ImageWidth" type="xs:int" />
        <xs:attribute default="-1" name="ImageHeight" type="xs:int" />
    </xs:complexType>
    <xs:simpleType name="ComicPageType">
        <xs:list>
            <xs:simpleType>
                <xs:restriction base="xs:string">
                    <xs:enumeration value="FrontCover" />
                    <xs:enumeration value="InnerCover" />
                    <xs:enumeration value="Roundup" />
                    <xs:enumeration value="Story" />
                    <xs:enumeration value="Advertisement" />
                    <xs:enumeration value="Editorial" />
                    <xs:enumeration value="Letters" />
                    <xs:enumeration value="Preview" />
                    <xs:enumeration value="BackCover" />
                    <xs:enumeration value="Other" />
                    <xs:enumeration value="Deleted" />
                </xs:restriction>
            </xs:simpleType>
        </xs:list>
    </xs:simpleType>
</xs:schema>

如果要将这份元数据文件内嵌进 CBR/CBZ 的漫画里,手动编辑是很不现实的。

目前我找到的一种方法是:在 Calibre 中,使用 Embed Comic Metadata 插件,在编辑完漫画元数据后,利用该插件将 ComicInfo.xml 内嵌至漫画文件中。

在「首选项」「插件」中搜索「Embed Comic Metadata」安装该插件。

安装完成后,在 Calibre 的导航栏会出现 Embed Comic Metadata 的操作按钮。

需要先补充完整元数据,可以使用 Calibre 内置的工具下载元数据。或者,前往国内漫画网站手动查询漫画数据。

一切准备就绪后,返回主界面,不用任何设置,直接导入即可。

插入元数据
插入元数据

内嵌元数据很方便,但是需要先导入 Calibre 编辑元数据才行。我觉得这样还是很麻烦的。

被导入元数据的漫画文件,可以看到如下内容:

嵌入的 ComicInfo.xml 内容如下:

<?xml version="1.0"?>
<ComicInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Title>隐瞒之事 (1)</Title>
  <Series>隐瞒之事</Series>
  <Number>1.0</Number>
  <Summary>隐瞒之事是画图的工作──! ?漫画家后藤可久士老师正在周刊少年漫画杂志上连载有点下流的漫画!但是一想到这件事可能会被就读小学4年级的独生女.姬发现,他就害怕得每天晚上都睡不好……。爱与欢笑的漫画家爸爸?女儿故事、就此开幕!
</Summary>
  <Year>2017</Year>
  <Month>11</Month>
  <Day>14</Day>
  <Writer>久米田康治</Writer>
  <Publisher>东立出版社</Publisher>
  <LanguageISO>zh</LanguageISO>
</ComicInfo>

我注意到压缩包注释中有个 ComicTagger 的字眼,一番搜索下,发现这就是个内嵌元数据的工具。遗憾的是,没有中文

看起来手动编辑似乎也不是很难,所以这里我就不把玩了。你有需求的话可以自行前往 ComicTagger 的 Github 主页 探索。

阅读

网页端的阅读就不介绍了,这里说一下其他设备。

Tachiyomi

在安卓系统上可以使用 Tachiyomi 阅读 Kmoga 中的漫画。

你可以在 Tachiyomi 的 Github Releases 页面下载最新版本的 App,在 插件下载页面 下载 Komga 插件。或是在 这里 下载我的备份。

安全完毕后,配置也相当简单。填入 Komga 的地址、用户名和密码,保存后重启 Tachiyomi 即可。

注意,账户密码只有重启后才会显示出来。

之后在图源中,打开 Komga 就能看到你的漫画了。

OPDS 阅读器

在 Komga 官方文档,对于 OPDS 阅读器的支持 是这么说明的:

Komga should work with any OPDS reader, unfortunately most readers badly implement the OPDS protocol 😞.

Komga 应该可以在任何 OPDS 阅读器上使用,遗憾的是,大多数阅读器都没有很好地遵循 OPDS 协议😞。

所以我建议安卓用户使用 Komga,苹果用户使用网页 XD。

如果真要用支持 OPDS 的阅读器观看 Komga 上的漫画,需要填写的 URL 格式为:

http(s)://your-server(:8080)(/baseUrl)/opds/v1.2/catalog

比如:

http://127.0.0.1:2333/opds/v1.2/catalog

总结

Komga 功能强大,目前我还没有彻底弄明白。而且如果真的想让 Komga 上的漫画展示得很好看的话,手动内嵌数据是逃不了的,这会是个十分漫长的过程……

参考