静看光阴荏苒
不管不顾不问不说也不念

自建视频聚合平台TypeType(支持B站/油管/NicoNico)

TypeType功能:

  • 通过自托管的 Web 应用程序播放 YouTube、NicoNico 和 BiliBili 视频。
  • 将历史记录、订阅、播放列表、收藏夹、稍后观看、进度和设置存储在您自己的实例中。
  • 通过后端 API 进行搜索、加载热门资讯、显示评论和打开频道页面。
  • 将视频保存到本地,通过独立的下载服务运行下载任务。
  • 多用户支持

这个项目有点意思,它没有选择目前最广泛使用的yt-dlp来实现相关功能,取而代之的是使用PipePipeExtractor。我在手动部署后体验了一下,给我的感觉是目前除了UI有点糙以外,这不是妥妥的神器,光无广告看油管就很爽了,还能下载视频到本地,还支持订阅、推送通知等等。只能说功能真的很全面了,就像个第三方的油管Web客户端。

该项目架构也非常清晰,作者声称纯古法编程,不使用AI:TypeType(前端),TypeType-Server(后端),TypeType-Downloader (下载服务),TypeType-Token (YouTube Proof-of-Origin令牌服务)全部都完整开源。

还贴心的提供了一键部署脚本:

curl -fsSL https://raw.githubusercontent.com/Priveetee/TypeType/main/scripts/install-stack.sh | bash

但是这篇文章我想记录下纯手动部署的方法,因为一键部署的方案不适合我,且听我细嗦一下原因:

1.有很多无用的环境变量,以及无用的端口暴露。

2.使用了两个PostgreSQL数据库容器,其中有一个是专门用来创建第二个数据库用的,实际不存储数据,我觉得这个创建多数据库的方法不太优雅,完全可以合并到一个容器内。

3.我不使用官方默认提供的Garage S3服务,因为这个Garage S3日常维护很麻烦,我选择用VersityGW替代。或者你不想把下载的视频存储到服务器本地,你也可以通过手动部署的方式配置其它的S3存储。

4.这也是最关键的一点,我这台服务器的IP被油管拉黑了,这导致TypeType根本无法播放油管视频,后端日志一直显示“登录证明不是bot”,我也是很服。不过我折腾的mihomo透明代理正好可以派上用场了,就当是实战演练了,看看对于这种复杂的compose编排环境,mihomo透明代理到底是能有多爽。这是一个牵一发而动全身的改动,要用mihomo透明代理,很多配置都需要修改,一键脚本不再可靠。

5.在配置反向代理后,前端容器无法获取客户端的真实IP,为解决这个问题,我修改了前端容器的nginx.conf文件。

如果你决定使用我这篇文章的方法来部署TypeType,我建议先阅读这两篇文章:1016510154先了解mihomo透明代理和VersityGW部署。

安装NGINX、CertBot、Docker:

apt update
apt install curl nginx python3-certbot-nginx
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

新建compose文件:

mkdir /opt/typetype-with-mihomo && cd /opt/typetype-with-mihomo && nano docker-compose.yml

写入如下内容:

services:
  mihomo:
    image: metacubex/mihomo:latest
    container_name: mihomo
    restart: always
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    volumes:
      - ./mihomo_config:/root/.config/mihomo
    ports:
      - "19090:9090"
      - "8082:80" 
      - "8081:8081"

  typetype:
    image: ghcr.io/priveetee/typetype:latest
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - typetype-server
    restart: unless-stopped
    network_mode: "service:mihomo"

  typetype-server:
    image: ghcr.io/priveetee/typetype-server:latest
    environment:
      DOWNLOADER_SERVICE_URL: http://127.0.0.1:18093
      ALLOWED_ORIGINS: "http://8.9.6.4:8082"
      DATABASE_URL: "jdbc:postgresql://127.0.0.1:5432/typetype"
      DATABASE_USER: "typetype"
      DATABASE_PASSWORD: "setyourpgpasswd"
      DRAGONFLY_URL: "redis://127.0.0.1:6379"
    depends_on:
      postgres:
        condition: service_started
      dragonfly:
        condition: service_started
      typetype-token:
        condition: service_started
      typetype-downloader:
        condition: service_started
    restart: unless-stopped
    network_mode: "service:mihomo"

  typetype-downloader:
    image: ghcr.io/priveetee/typetype-downloader:latest
    environment:
      HTTP_PORT: "18093"
      PUBLIC_BASE_URL: /api/downloader
      TYPETYPE_API_BASE: http://127.0.0.1:8080
      DB_URL: jdbc:postgresql://127.0.0.1:5432/typetype_downloader
      DB_USER: typetype
      DB_PASSWORD: setyourpgpasswd
      REDIS_HOST: 127.0.0.1
      REDIS_PORT: "6379"
      REDIS_QUEUE_KEY: downloader:queue
      MAX_CONCURRENT_WORKERS: "2"
      MAX_QUEUE_SIZE: "100"
      JOB_TTL_SECONDS: "600"
      DOWNLOAD_WORKERS: "8"
      DOWNLOAD_CHUNK_SIZE: "10485760"
      DOWNLOAD_RANGE_MODE: query
      MUXER: avformat
      STORAGE_BACKEND: s3
      S3_ENDPOINT: https://versitygw-s3.example.com
      S3_PUBLIC_ENDPOINT: https://versitygw-s3.example.com
      S3_REGION: us-east-1
      S3_BUCKET: typetype-downloads
      S3_ACCESS_KEY: hidden
      S3_SECRET_KEY: "hidden"
      S3_ARTIFACT_TTL_SECONDS: "7200"
    depends_on:
      postgres:
        condition: service_started
      dragonfly:
        condition: service_started
      typetype-token:
        condition: service_started
    restart: unless-stopped
    network_mode: "service:mihomo"

  typetype-token:
    image: ghcr.io/priveetee/typetype-token:latest
    init: true
    ipc: host
    environment:
      - NODE_ENV=production
    restart: unless-stopped
    network_mode: "service:mihomo"

  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: typetype
      POSTGRES_USER: typetype
      POSTGRES_PASSWORD: setyourpgpasswd
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
      - ./init-multiple-databases.sql:/docker-entrypoint-initdb.d/init-multiple-databases.sql:ro
    restart: unless-stopped
    network_mode: "service:mihomo"

  dragonfly:
    image: docker.dragonflydb.io/dragonflydb/dragonfly:latest
    ulimits:
      memlock: -1
    restart: unless-stopped
    network_mode: "service:mihomo"

这个compose内的配置我觉得有必要详细说一下,不然很可能让人觉得一头雾水。首先要知道这些服务自身的端口,哪些端口需要暴露出来,哪些不需要暴露仅供容器内部互访:

typetype --> 8082:80 # 必须暴露
typetype-server --> 8080:8080 # 无须暴露
typetype-downloader --> 18093:18093 # 无须暴露
typetype-token --> 8081:8081 # 必须暴露
postgres --> 5432:5432 # 无须暴露
dragonfly --> 6379:6379 # 无须暴露

由于全部服务都使用network_mode: "service:mihomo"来实现透明代理,所以必须作出这些改动:需要暴露端口的服务,必须把端口映射配置写在mihomo服务的ports列表里。不需要暴露的端口全部用于服务之间互访,服务之间不能再通过服务名来访问,必须改为127.0.0.1。

因此typetype-server的下载服务URL、PG数据库、Dragonfly数据库地址都需要改:

services:
  typetype-server:
    image: ghcr.io/priveetee/typetype-server:latest
    environment:
      DOWNLOADER_SERVICE_URL: http://127.0.0.1:18093
      DATABASE_URL: "jdbc:postgresql://127.0.0.1:5432/typetype"
      DRAGONFLY_URL: "redis://127.0.0.1:6379"

还有typetype-downloader的后端URL,PG数据库、Dragonfly数据库地址:

services:
  typetype-downloader:
    image: ghcr.io/priveetee/typetype-downloader:latest
    environment:
      HTTP_PORT: "18093"
      PUBLIC_BASE_URL: /api/downloader
      TYPETYPE_API_BASE: http://127.0.0.1:8080
      DB_URL: jdbc:postgresql://127.0.0.1:5432/typetype_downloader
      REDIS_HOST: 127.0.0.1
      REDIS_PORT: "6379"

我的示例配置已经这样配置了,这里只是说明为什么需要这么配置,以及为什么这么配置多个服务之间能够正常工作,就算你不改直接照搬也能用。而接下来要说的内容是必须要修改的。首先得给typetype-downloader配置s3存储:

services:
  typetype-downloader:
    image: ghcr.io/priveetee/typetype-downloader:latest
    environment:
      STORAGE_BACKEND: s3
      S3_ENDPOINT: https://versitygw-s3.example.com
      S3_PUBLIC_ENDPOINT: https://versitygw-s3.example.com
      S3_REGION: us-east-1
      S3_BUCKET: typetype-downloads
      S3_ACCESS_KEY: hidden
      S3_SECRET_KEY: "hidden"
      S3_ARTIFACT_TTL_SECONDS: "7200"

配置typetype-server的CORS,否则后端将拒绝(403)不在列表里的前端(域名)请求后端。如果不使用反向代理就配置成服务器公网IP:

services:
  typetype-server:
    image: ghcr.io/priveetee/typetype-server:latest
    environment:
      ALLOWED_ORIGINS: "http://8.9.6.4:8082"

配置多个:

services:
  typetype-server:
    image: ghcr.io/priveetee/typetype-server:latest
    environment:
      ALLOWED_ORIGINS: "http://8.9.6.4:8082,https://typetype.example.com"

compose的配置就全部完成了,现在新建一个nginx.conf,这是TypeType前端服务(容器)需要用到的NGINX配置文件:

nano nginx.conf

写入如下内容:

server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;
    client_max_body_size 2g;
    gzip on;
    gzip_types text/plain text/css application/javascript application/json image/svg+xml;

    set_real_ip_from  10.0.0.0/8;
    set_real_ip_from  172.16.0.0/12;
    real_ip_header    X-Forwarded-For; 
    real_ip_recursive on;

    location ^~ /api/ {
        client_max_body_size 2g;
        proxy_pass http://127.0.0.1:8080/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_buffering off;
    }

    location / {
        try_files $uri $uri/ /index.html;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

这里有几个细节,后端地址必须使用127.0.0.1:8080,而不是typetype-server:8080,原因之前说过在使用network_mode: "service:mihomo"后容器之间不能再通过服务名来访问:

location ^~ /api/ {
    client_max_body_size 2g;
    proxy_pass http://127.0.0.1:8080/;
}

在使用主机的NGINX反向代理后,前端容器获取不到客户端的真实IP,所以我加了如下内容到配置文件:

set_real_ip_from  10.0.0.0/8;
set_real_ip_from  172.16.0.0/12;
real_ip_header    X-Forwarded-For; 
real_ip_recursive on;

该项目的设计架构是后端服务需要用到一个数据库,下载服务也需要一个数据库,所以这里新建一个.sql文件,创建第二个数据库用于下载服务:

nano init-multiple-databases.sql

写入如下内容:

CREATE DATABASE typetype_downloader;

还需要新建一个目录用于存放mihomo的配置文件和其它资源(如:控制面板文件、rule-set文件)

mkdir mihomo_config

新建mihomo的配置文件:

nano mihomo_config/config.yaml

这是我的示例配置:

mixed-port: 7890
allow-lan: true
tcp-concurrent: true
find-process-mode: strict
mode: rule
log-level: info
ipv6: false
keep-alive-interval: 30
unified-delay: true

profile:
  store-selected: true
  store-fake-ip: false

external-controller: 0.0.0.0:9090
external-controller-cors:
  allow-origins:
    - '*'
  allow-private-network: true
secret: "89641937"             
external-ui: "./ui"                      
external-ui-name: zashboard
external-ui-url: "https://github.com/Zephyruso/zashboard/archive/refs/heads/gh-pages.zip"

tun:
  enable: true
  stack: mixed
  auto-route: true
  auto-redirect: false
  auto-detect-interface: true
  dns-hijack:
    - any:53
  strict-route: true
  mtu: 1500

dns:
  enable: true
  cache-algorithm: arc
  prefer-h3: false
  use-hosts: true
  use-system-hosts: true
  listen: 127.0.0.1:6868
  ipv6: false
  enhanced-mode: redir-host
  default-nameserver:
    - 8.8.8.8
    - 1.1.1.1
  nameserver:
    - https://cloudflare-dns.com/dns-query
    - https://dns.google/dns-query
  proxy-server-nameserver:
    - https://cloudflare-dns.com/dns-query
    - https://dns.google/dns-query
  direct-nameserver:
    - https://dns.google/dns-query
    - https://cloudflare-dns.com/dns-query
  respect-rules: true

sniffer:
  enable: true
  force-dns-mapping: true
  parse-pure-ip: true
  sniff:
    HTTP:
      ports:
        - 80
        - 8080-8880
      override-destination: true
    TLS:
      ports:
        - 443
        - 8443

proxies:
  - name: proxy1
    type: anytls
    server: 8.9.6.4
    port: 8443
    password: "hidden"
    client-fingerprint: chrome
    udp: true
    idle-session-check-interval: 30
    idle-session-timeout: 30
    min-idle-session: 5
    sni: "anytls.example.com"
    alpn:
      - h2
    skip-cert-verify: false

proxy-groups:
  - name: PROXY
    icon: https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Hijacking.png
    type: select
    proxies:
      - proxy1

rule-providers:
  geosite-youtube:
    type: http
    behavior: domain
    format: mrs
    url: https://github.com/MetaCubeX/meta-rules-dat/raw/meta/geo/geosite/youtube.mrs
    path: ./rule-sets/youtube.mrs
    interval: 86400
    proxy: proxy1

rules:
  - DOMAIN-SUFFIX,ipinfo.io,PROXY
  - RULE-SET,geosite-youtube,PROXY
  - MATCH,DIRECT

该配置并非最佳实践,里面包含一些无用或者多余的配置项,但并不会影响实际使用。

现在启动所有服务:

docker compose up -d

配置NGINX反向代理:

nano /etc/nginx/sites-available/typetype

写入如下内容:

server {
    listen 80;
    server_name typetype.example.com;

    location / {
        proxy_pass http://127.0.0.1:8082;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

启用站点:

ln -s /etc/nginx/sites-available/typetype /etc/nginx/sites-enabled/typetype

签发证书:

certbot --nginx

测试下效果,油管视频可正常播放:

b站视频可正常播放:

这个东西牛逼之处就在于你可以按需来跳过自己不想看到的内容,比如广告,自推销:

视频下载我也测试了一下,油管的视频可以下载,b站的不行,应该是有BUG:

s3桶里有下载的视频文件:

他这个视频下载实现的逻辑是先把视频下载保存到s3,再从s3获取一个预签名的下载链接。我觉得这块可以优化一下,比如视频下载保存到s3后,再次播放就不走源站了,直接从s3播放,这样的话可以防止原视频失效,也能让保存在s3内的视频发挥作用。总的来说我觉得这个程序在细节上还有很多地方值得完善。

再就是你可以看到这样一个项目,前端TypeScript,后端Kotlin,下载服务Golang,无论容器内的App用什么语言,mihomo都能完美透明代理,简直不要太爽。我还学到一个小技巧,虽然最后没派上用场还是上代理解决的,但是我觉得这个方案在某些时候应该是可行的,既然IPv4被油管拉黑了,那不妨试试IPv6。既然后端是Kotlin,那可以设置一个环境变量让其优先使用IPv6访问:

services:
  typetype-server:
    image: ghcr.io/priveetee/typetype-server:latest
    environment:
      _JAVA_OPTIONS: "-Djava.net.preferIPv6Addresses=true"

然后你就会掉到另一个坑里面,后端连不上Dragonfly数据库了!因为它没监听IPv6地址,你还得改一下配置:

services:
  dragonfly:
    image: docker.dragonflydb.io/dragonflydb/dragonfly:latest
    command: ["--bind", "0.0.0.0", "--bind", "::"]

除此之外,你还要在这个compose里面启用IPv6:

services:
  typetype:
    image: ghcr.io/priveetee/typetype:latest
    networks:
      - typetype-net

  typetype-server:
    image: ghcr.io/priveetee/typetype-server:latest
    networks:
      - typetype-net

  typetype-downloader:
    image: ghcr.io/priveetee/typetype-downloader:latest
    networks:
      - typetype-net

  typetype-token:
    image: ghcr.io/priveetee/typetype-token:latest
    networks:
      - typetype-net

  postgres:
    image: postgres:17
    networks:
      - typetype-net

  dragonfly:
    image: docker.dragonflydb.io/dragonflydb/dragonfly:latest
    networks:
      - typetype-net

networks:
  typetype-net:
    enable_ipv6: true

最后的最后,你可能还需要为Docker启用IPv6 NAT,方法见这篇文章。我试了我的IPv6也被油管拉黑了,所以这是一个失败的尝试。我还是建议能用mihomo就直接用mihomo。

赞(0)
未经允许不得转载:荒岛 » 自建视频聚合平台TypeType(支持B站/油管/NicoNico)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

分享创造快乐

广告合作资源投稿