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,我建议先阅读这两篇文章:10165、10154先了解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。
荒岛




















