群晖部署-Calibreweb

最近家里搞了台群晖,准备部署个Calibreweb看看电子书,看了好多部署的教程老是不太成功,要么报错要么有问题的,很难搞。下面将部署流程分享一下,给大家参考,少走点弯路

镜像的选择

我们使用johngong/calibre-web,我们用这个镜像来构建应用,这个镜像已经添加了一些原始镜像没有的功能,可以更快的上手。我们直接拉取johngong/calibre-web镜像即可

配置

映射卷

我们需要映射镜像的卷
在这里插入图片描述
在这里插入图片描述

参数说明
本地文件夹1:/librarycalibre-web与calibre-server书库默认位置
本地文件夹2:/configcalibre-web与calibre-server配置位置文件
本地文件夹3:/autoaddbookscalibre自动添加图书文件夹位置
本地文件夹4:/usr/local/calibre-web/app/metadata_provider元数据抓取脚本位置

本地文件夹4:/usr/local/calibre-web/app/metadata_provider
这条配置下的本地文件夹4,需要放一个获取元数据的脚本,脚本内容如下,命名为NewDouban.py

import random
import re
import time
import dataclasses
import urllib

import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urlparse, unquote
from lxml import etree
from functools import lru_cache

from cps.services.Metadata import Metadata, MetaSourceInfo, MetaRecord

from cps.search_metadata import meta
from flask import request, Response
from cps import helper

# 是否自动代理封面地址
DOUBAN_PROXY_COVER = True
# 如果自动计算的服务器地址不正确,可以填写自己的calibre-web地址,参考:http://nas_ip:8083/
DOUBAN_PROXY_COVER_HOST_URL = ''
DOUBAN_PROXY_COVER_PATH = 'metadata/douban_cover?cover='
DOUBAN_SEARCH_URL = "https://www.douban.com/search"
DOUBAN_BASE = "https://book.douban.com/"
DOUBAN_COVER_DOMAIN = 'doubanio.com'
DOUBAN_BOOK_CAT = "1001"
DOUBAN_BOOK_CACHE_SIZE = 500  # 最大缓存数量
DOUBAN_CONCURRENCY_SIZE = 5  # 并发查询数
DOUBAN_BOOK_URL_PATTERN = re.compile(".*/subject/(\\d+)/?")
DEFAULT_HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3573.0 Safari/537.36',
    'Accept-Encoding': 'gzip, deflate',
    'Referer': DOUBAN_BASE
}
PROVIDER_NAME = "New Douban Books"
PROVIDER_ID = "new_douban"


class NewDouban(Metadata):
    __name__ = PROVIDER_NAME
    __id__ = PROVIDER_ID

    def __init__(self):
        self.searcher = DoubanBookSearcher()
        self.hack_helper_cover()
        super().__init__()

    def search(self, query: str, generic_cover: str = "", locale: str = "en"):
        if self.active:
            return self.searcher.search_books(query)

    @staticmethod
    def hack_helper_cover():
        """
        覆盖helper.save_cover_from_url方法实现豆瓣的封面下载
        :return:
        """
        save_cover = helper.save_cover_from_url

        def new_save_cover(url, book_path):
            if DOUBAN_COVER_DOMAIN in url:
                cover_url = url
                if DOUBAN_PROXY_COVER:
                    component = urllib.parse.urlparse(url)
                    query = urllib.parse.parse_qs(component.query)
                    cover_url = urllib.parse.unquote(query.get('cover')[0])
                res = requests.get(cover_url, headers=DEFAULT_HEADERS)
                return helper.save_cover(res, book_path)
            else:
                return save_cover(url, book_path)

        helper.save_cover_from_url = new_save_cover


@dataclasses.dataclass
class DoubanMetaRecord(MetaRecord):

    def __getattribute__(self, item):  # cover通过本地服务代理访问
        if item == 'cover' and DOUBAN_PROXY_COVER:
            cover_url = super().__getattribute__(item)
            if cover_url:
                try:
                    host_url = DOUBAN_PROXY_COVER_HOST_URL
                    if not host_url and request.host_url:
                        host_url = request.host_url
                    if host_url and host_url not in cover_url:
                        self.cover = host_url + DOUBAN_PROXY_COVER_PATH + urllib.parse.quote(cover_url)
                except BaseException:
                    pass
        return super().__getattribute__(item)


class DoubanBookSearcher:

    def __init__(self):
        self.book_loader = DoubanBookLoader()
        self.thread_pool = ThreadPoolExecutor(max_workers=10, thread_name_prefix='douban_async')

    def calc_url(self, href):
        query = urlparse(href).query
        params = {item.split('=')[0]: item.split('=')[1] for item in query.split('&')}
        url = unquote(params['url'])
        if DOUBAN_BOOK_URL_PATTERN.match(url):
            return url

    def load_book_urls_new(self, query):
        url = DOUBAN_SEARCH_URL
        params = {"cat": DOUBAN_BOOK_CAT, "q": query}
        res = requests.get(url, params, headers=DEFAULT_HEADERS)
        book_urls = []
        if res.status_code in [200, 201]:
            html = etree.HTML(res.content)
            alist = html.xpath('//a[@class="nbg"]')
            for link in alist:
                href = link.attrib['href']
                parsed = self.calc_url(href)
                if parsed and len(book_urls) < DOUBAN_CONCURRENCY_SIZE:
                    book_urls.append(parsed)
        return book_urls

    def search_books(self, query):
        book_urls = self.load_book_urls_new(query)
        books = []
        futures = [self.thread_pool.submit(self.book_loader.load_book, book_url) for book_url in book_urls]
        for future in as_completed(futures):
            book = future.result()
            if book is not None:
                books.append(future.result())
        return books


class DoubanBookLoader:

    def __init__(self):
        self.book_parser = DoubanBookHtmlParser()

    @lru_cache(maxsize=DOUBAN_BOOK_CACHE_SIZE)
    def load_book(self, url):
        book = None
        self.random_sleep()
        start_time = time.time()
        res = requests.get(url, headers=DEFAULT_HEADERS)
        if res.status_code in [200, 201]:
            print("下载书籍:{}成功,耗时{:.0f}ms".format(url, (time.time() - start_time) * 1000))
            book_detail_content = res.content
            book = self.book_parser.parse_book(url, book_detail_content.decode("utf8"))
        return book

    def random_sleep(self):
        random_sec = random.random() / 10
        print("Random sleep time {}s".format(random_sec))
        time.sleep(random_sec)

class DoubanBookHtmlParser:
    def __init__(self):
        self.id_pattern = DOUBAN_BOOK_URL_PATTERN
        self.date_pattern = re.compile("(\\d{4})-(\\d+)")
        self.tag_pattern = re.compile("criteria = '(.+)'")

    def parse_book(self, url, book_content):
        book = DoubanMetaRecord(
            id="",
            title="",
            authors=[],
            publisher="",
            description="",
            url="",
            source=MetaSourceInfo(
                id=PROVIDER_ID,
                description=PROVIDER_NAME,
                link="https://book.douban.com/"
            )
        )
        html = etree.HTML(book_content)
        title_element = html.xpath("//span[@property='v:itemreviewed']")
        book.title = self.get_text(title_element)
        share_element = html.xpath("//a[@data-url]")
        if len(share_element):
            url = share_element[0].attrib['data-url']
        book.url = url
        id_match = self.id_pattern.match(url)
        if id_match:
            book.id = id_match.group(1)
        img_element = html.xpath("//a[@class='nbg']")
        if len(img_element):
            cover = img_element[0].attrib['href']
            if not cover or cover.endswith('update_image'):
                book.cover = ''
            else:
                book.cover = cover
        rating_element = html.xpath("//strong[@property='v:average']")
        book.rating = self.get_rating(rating_element)
        elements = html.xpath("//span[@class='pl']")
        for element in elements:
            text = self.get_text(element)
            if text.startswith("作者") or text.startswith("译者"):
                book.authors.extend([self.get_text(author_element) for author_element in
                                     filter(self.author_filter, element.findall("..//a"))])
            elif text.startswith("出版社"):
                book.publisher = self.get_tail(element)
            elif text.startswith("副标题"):
                book.title = book.title + ':' + self.get_tail(element)
            elif text.startswith("出版年"):
                book.publishedDate = self.get_publish_date(self.get_tail(element))
            elif text.startswith("丛书"):
                book.series = self.get_text(element.getnext())
            elif text.startswith("ISBN"):
                book.identifiers["isbn"] = self.get_tail(element)
        summary_element = html.xpath("//div[@id='link-report']//div[@class='intro']")
        if len(summary_element):
            book.description = etree.tostring(summary_element[-1], encoding="utf8").decode("utf8").strip()
        tag_elements = html.xpath("//a[contains(@class, 'tag')]")
        if len(tag_elements):
            book.tags = [self.get_text(tag_element) for tag_element in tag_elements]
        else:
            book.tags = self.get_tags(book_content)
        return book

    def get_tags(self, book_content):
        tag_match = self.tag_pattern.findall(book_content)
        if len(tag_match):
            return [tag.replace('7:', '') for tag in
                    filter(lambda tag: tag and tag.startswith('7:'), tag_match[0].split('|'))]
        return []

    def get_publish_date(self, date_str):
        if date_str:
            date_match = self.date_pattern.fullmatch(date_str)
            if date_match:
                date_str = "{}-{}-1".format(date_match.group(1), date_match.group(2))
        return date_str

    def get_rating(self, rating_element):
        return float(self.get_text(rating_element, '0')) / 2

    def author_filter(self, a_element):
        a_href = a_element.attrib['href']
        return '/author' in a_href or '/search' in a_href

    def get_text(self, element, default_str=''):
        text = default_str
        if len(element) and element[0].text:
            text = element[0].text.strip()
        elif isinstance(element, etree._Element) and element.text:
            text = element.text.strip()
        return text if text else default_str

    def get_tail(self, element, default_str=''):
        text = default_str
        if isinstance(element, etree._Element) and element.tail:
            text = element.tail.strip()
            if not text:
                text = self.get_text(element.getnext(), default_str)
        return text if text else default_str


@meta.route("/metadata/douban_cover", methods=["GET"])
def proxy_douban_cover():
    """
    代理豆瓣封面展示
    :return:
    """
    cover_url = urllib.parse.unquote(request.args.get('cover'))
    res = requests.get(cover_url, headers=DEFAULT_HEADERS)
    return Response(res.content, mimetype=res.headers['Content-Type'])

映射端口

参数说明
本地端口1:8083calibre-web web访问端口
本地端口2:8080calibre-server web访问端口

环境变量

环境变量默认即可 大家可以根据需要进行调整

参数说明
UID=1000uid设置,默认为1000
GID=1000gid设置,默认为1000
ENABLE_CALIBRE_SERVER=true(true|false)设定开启calibre-server,默认开启
ENABLE_CALIBRE_SERVER_OPDS=false(true|false)开启calibre-server的OPDS功能,默认不开启,arm可能不可用
CALIBRE_SERVER_USER=calibre-server 用户名
CALIBRE_SERVER_PASSWORD=calibre-server 用户密码
CALIBRE_SERVER_WEB_LANGUAGE=zh_CNcalibre-server web界面语言,详见calibre-server其它语言
CALIBRE_ASCII_FILENAME=true(true|false)设定false时calibre支持中文目录
CALIBRE_WEB_LANGUAGE=zh_Hans_CN(zh_Hans_CN|en)calibre-web初始界面语言,详见calibre-web其它语言
TZ=Asia/Shanghai系统时区设置,默认为Asia/Shanghai
CALIBREDB_OTHER_OPTION=为自动添加脚本中calibredb命令添加其它参数,例如:duplicates命令[-d]
DISABLE_GOOGLE_SEARCH=false(true|false)设定禁用google搜索,默认不开启
DISABLE_SCHOLAR_SEARCH=false(true|false)设定禁用scholar搜索,默认不开启
ENABLE_CHOWN_LIBRARY=true(true|false)设定修复library文件夹拥有者,默认开启
CALIBRE_LOCALHOST=true(true|false)设定从本地主机和本地网络加载封面,默认开启

配置完成就可以正常启动了
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/958238.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

WordPress果果对象存储插件

将网站上的图片等静态资源文件上传至七牛云对象存储&#xff0c;可以减轻服务器文件存储压力&#xff0c;提升静态文件访问速度&#xff0c;从而加速网站访问速度。 支持&#xff1a;阿里云对象存储、华为云对象存储、百度云对象存储、腾讯云对象存储、七牛云对象存储。 下载…

电路研究9.1.1——合宙 Air780EP 模组外围线路

本来要继续研究AT指令来着&#xff0c;结果发现后面还有之前用到的电路设计资料&#xff0c;所以就贴过来了。 5.3.2 工作模式&#xff1a; 注意&#xff1a;  当模块进入休眠模式或深度休眠模式后&#xff0c; VDD_EXT 电源会掉电&#xff0c;相应电压域的 GPIO 以及串口…

LabVIEW 太阳能光伏发电系统智能监控

本文介绍了基于 LabVIEW 的太阳能光伏发电监控系统的设计与实现&#xff0c;着重探讨了其硬件配置、软件架构以及系统的实现方法。该系统能够有效提高太阳能光伏发电的监控效率和精确性&#xff0c;实现了远程监控和数据管理的智能化。 ​ 项目背景 在当前能源紧张与环境污染…

风光并网对电网电能质量影响的matlab/simulink仿真建模

这个课题早在一几年的时候比较热门&#xff0c;之前作电科院配电网的一个项目中也有所涉及&#xff0c;我把其中一部分经典仿真模型思路分享给大家&#xff0c;电能质量影响这部分&#xff0c;我在模型中主要体现的就是不同容量的光伏、风电接入&#xff0c;对并网点的电压影响…

大模型应用编排工具Dify之常用编排组件

1.前言 dify的核心能力有&#xff1a;支持接入常见的 LLM、工作流编排、知识库和聊天助手等&#xff0c;架构图如下&#xff1a; 本文将结合实际项目落地经验&#xff0c;针对工作流编排中的常用编排组件进行介绍&#xff0c;以及如何在后端调用工作流编排。 2.落地案例 某 …

Ubuntu16.04 安装OpenCV4.5.4 避坑

Ubuntu16.04 安装C版OpenCV4.5.4 Ubuntu16.04 VSCode下cmakeclanglldb调试c 文章目录 Ubuntu16.04 安装C版OpenCV4.5.41. 下载Opencv压缩包2. 安装Opencv-4.5.43. 配置OpenCV的编译环境4.测试是否安装成功 1. 下载Opencv压缩包 下载Opencv压缩包&#xff0c;选择source版本。…

pytest执行报错:found no collectors

今天在尝试使用pytest运行用例的时候出现报错&#xff1a;found no collectors&#xff1b;从两个方向进行排查&#xff0c;一是看文件名和函数名是不是符合规范&#xff0c;命名要是"test_*"格式&#xff1b;二是是否存在修改文件名的情况&#xff0c;如果修改过文件…

嵌入式知识点总结 C/C++ 专题提升(七)-位操作

针对于嵌入式软件杂乱的知识点总结起来&#xff0c;提供给读者学习复习对下述内容的强化。 目录 1.位操作基础 2.如何求解整型数的二进制表示中1的个数 ? 3.如何求解二进制中0的个数 4.交换两个变量的值&#xff0c;不使用第三个变量。即a3,b5,交换之后a5,b3: 5.给定一个…

两台局域网电脑通过飞秋传输大文件失败的解决方案

问题描述&#xff1a; 局域网两台电脑之间传输大文件&#xff08;超过20G&#xff09;&#xff0c;不想太复杂&#xff0c;就各装个飞秋。但是通过直接发送文件发现总是失败&#xff0c;一会就中断了。 解决方法&#xff1a; 主界面上有一个文件共享的按钮&#xff0c;通过文…

Picsart美易照片编辑器和视频编辑器

使用Picsart美易照片编辑器和视频编辑器&#xff0c;将您的创意变为现实。制作专业水准的拼贴画、设计并添加贴纸、快速移除和更换背景&#xff0c;体验流行编辑&#xff0c;比如 黄金时刻、镜中自拍、复古噪点滤镜或千禧滤镜。Picsart美易是一款一体式编辑器&#xff0c;拥有众…

AR智慧点巡检系统探究和技术方案设计

一、项目背景 随着工业生产规模的不断扩大和设备复杂度的提升&#xff0c;传统的人工点巡检方式效率低下、易出错&#xff0c;难以满足现代化企业对设备运行可靠性和安全性的要求。AR&#xff08;增强现实&#xff09;技术的发展为点巡检工作带来了新的解决方案&#xff0c;通…

游戏设备升级怎么选?RTX4070独显,ToDesk云电脑更具性价比

过新年、添喜气&#xff01;正逢节期来临不知道各位是否都跟小编一样在考虑购置生活中的各样所需呐&#xff1f; 25年可谓是3A游戏大作之年&#xff0c;例如《GTA6》《文明7》《死亡搁浅2》《刺客信条&#xff1a;影》下半年落地的《塞尔达传说&#xff1a;新篇章》《生化危机9…

算法刷题笔记——图论篇

这里写目录标题 理论基础图的基本概念图的种类度 连通性连通图强连通图连通分量强连通分量 图的构造邻接矩阵邻接表 图的遍历方式 深度优先搜索理论基础dfs 与 bfs 区别dfs 搜索过程深搜三部曲所有可达路径广度优先搜索理论基础广搜的使用场景广搜的过程 岛屿数量孤岛的总面积沉…

怎么使用python 调用高德地图api查询位置和导航?

环境&#xff1a; python 3.10 问题描述&#xff1a; 怎么使用python 调用高德地图api查询位置和导航? 解决方案&#xff1a; 要使用Python调用高德地图API查询位置和导航&#xff0c;需要先注册高德开发者账号并获取API Key。以下是基本步骤&#xff1a; 1. 注册高德开…

【阿里云】使用docker安装nginx后可以直接访问

一、创建目录 mkdir -p config/{cert,conf.d} html logs二、上传nginx.conf的配置文件 user nginx; worker_processes auto;error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid;events {worker_connections 1024; }http {include /etc/ngin…

Fisco-Bcos-java-SDK 利用java与fisco-Bcos区块链上的智能合约交互(以HelloWorld为例)

Fisco-Bcos-java-SDK 利用java与fisco-Bcos区块链上的智能合约交互&#xff08;以HelloWorld为例&#xff09; 一、部署智能合约 1、编写智能合约 此处用最简单的HelloWorld合约作为例子 包含两个方法和一个构造函数 构造函数&#xff1a;当合约部署的时候 执行构造函数 将…

DearMom婴儿车:书籍点亮希望,为乡村留守儿童架起知识桥梁

近日&#xff0c;DearMom婴儿车携手中国社会福利基金会来到河南上蔡赵庄小学&#xff0c;成功举办了一场意义非凡的公益助学活动&#xff0c;这是他们第二次以实际行动诠释企业社会责任。此次活动&#xff0c;品牌方致力于以书籍为媒介&#xff0c;为乡村留守儿童开拓视野、丰富…

备赛蓝桥杯之第十五届职业院校组省赛第二题:分享点滴

提示&#xff1a;本篇文章仅仅是作者自己目前在备赛蓝桥杯中&#xff0c;自己学习与刷题的学习笔记&#xff0c;写的不好&#xff0c;欢迎大家批评与建议 由于个别题目代码量与题目量偏大&#xff0c;请大家自己去蓝桥杯官网【连接高校和企业 - 蓝桥云课】去寻找原题&#xff0…

阿里云-银行核心系统转型之业务建模与技术建模

业务领域建模包括业务建模和技术建模&#xff0c;整体建模流程图如下&#xff1a; 业务建模包括业务流程建模和业务对象建模 业务流程建模&#xff1a;通过对业务流程现状分析&#xff0c;结合目标核心系统建设能力要求&#xff0c;参考行业建 模成果&#xff0c;形成结构化的…

吴恩达深度学习——如何实现神经网络

来自吴恩达深度学习&#xff0c;仅为本人学习所用。 文章目录 神经网络的表示计算神经网络的输出激活函数tanh选择激活函数为什么需要非激活函数双层神经网络的梯度下降法 随机初始化 神经网络的表示 对于简单的Logistic回归&#xff0c;使用如下的计算图。 如果是多个神经元…