Merge pull request #5483 from okxlin/batch-770/podfetch-20260801

feat(app): add PodFetch
This commit is contained in:
okxlin
2026-08-01 06:49:22 +08:00
committed by GitHub
17 changed files with 641 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
# PodFetch
## 产品介绍
PodFetch 是一个自托管的播客管理器和下载器,提供 Web 界面、全文检索以及多种移动端和订阅阅读器集成。
## 主要功能
- 订阅、刷新和下载公开播客源。
- 使用 SQLite 保存订阅、用户和播放状态。
- 将播客媒体保存到独立目录,便于备份或迁移。
- 支持基本认证、OIDC、GPodder 和 Audiobookshelf 兼容接口。
## 访问说明
- 默认启用基本认证,请使用安装表单中的管理员用户名和密码登录。
- `播客目录` 和 `数据库目录` 都是持久化边界,卸载应用时不会自动删除。
- 如使用反向代理,请启用 WebSocket 并转发标准的 `X-Forwarded-*` 头。
## Introduction
PodFetch is a self-hosted podcast manager and downloader with a web interface, full-text search, and integrations for mobile apps and feed readers.
## Features
- Subscribe to, refresh, and download public podcast feeds.
- Store subscriptions, users, and playback state in SQLite.
- Keep downloaded media in a separate directory for backup and migration.
- Supports Basic authentication, OIDC, GPodder, and Audiobookshelf-compatible APIs.
## 参考资料
- 文档:<https://samtv12345.github.io/PodFetch/>
- 源码:<https://github.com/SamTV12345/PodFetch>
+3
View File
@@ -0,0 +1,3 @@
# PodFetch
See `README.md` for the bilingual product description, configuration, and persistence notes.
+31
View File
@@ -0,0 +1,31 @@
name: PodFetch
tags:
- 多媒体
title: 自托管的播客管理器和下载器
description: 自托管的播客管理器和下载器
additionalProperties:
key: podfetch
name: PodFetch
tags:
- Media
shortDescZh: 自托管的播客管理器和下载器
shortDescEn: Self-hosted podcast manager and downloader
description:
en: Self-hosted podcast manager and downloader with web, mobile, and feed-reader integrations.
zh: 支持 Web、移动端和订阅阅读器集成的自托管播客管理器与下载器。
zh-Hant: 支援 Web、行動端和訂閱閱讀器整合的自託管播客管理器與下載器。
ja: Web、モバイル、フィードリーダー連携に対応したセルフホスト型ポッドキャスト管理・ダウンロードツールです。
ko: 웹, 모바일, 피드 리더 연동을 지원하는 자체 호스팅 팟캐스트 관리자 및 다운로더입니다.
ru: Самостоятельно размещаемый менеджер и загрузчик подкастов с интеграциями.
ms: Pengurus dan pemuat turun podcast dihos sendiri dengan integrasi web dan mudah alih.
pt-br: Gerenciador e baixador de podcasts auto-hospedado com integracoes web e moveis.
type: tool
crossVersionUpdate: true
limit: 0
recommend: 0
website: https://samtv12345.github.io/PodFetch/
github: https://github.com/SamTV12345/PodFetch
document: https://samtv12345.github.io/PodFetch/
architectures:
- amd64
- arm64
+11
View File
@@ -0,0 +1,11 @@
CONTAINER_NAME=podfetch
PANEL_APP_BIND_ADDRESS=0.0.0.0
PANEL_APP_PORT_HTTP=40100
PUID=1000
PGID=1000
POLLING_INTERVAL=60
BASIC_AUTH=true
PODFETCH_USERNAME=admin
PODFETCH_PASSWORD=replace-with-a-strong-password
PODCASTS_DIR=./data/podcasts
DATABASE_DIR=./data/db
+170
View File
@@ -0,0 +1,170 @@
additionalProperties:
formFields:
- default: 0.0.0.0
edit: true
envKey: PANEL_APP_BIND_ADDRESS
labelEn: Bind Address
labelZh: 绑定地址
label:
en: Bind Address
zh: 绑定地址
zh-Hant: 綁定位址
ja: バインドアドレス
ko: 바인드 주소
ru: Адрес привязки
ms: Alamat ikatan
pt-br: Endereco de vinculacao
required: true
type: text
- default: 40100
edit: true
envKey: PANEL_APP_PORT_HTTP
labelEn: HTTP Port
labelZh: HTTP 端口
label:
en: HTTP Port
zh: HTTP 端口
zh-Hant: HTTP 連接埠
ja: HTTP ポート
ko: HTTP 포트
ru: HTTP-порт
ms: Port HTTP
pt-br: Porta HTTP
required: true
rule: paramPort
type: number
- default: 1000
edit: true
envKey: PUID
labelEn: User ID
labelZh: 用户 ID
label:
en: User ID
zh: 用户 ID
zh-Hant: 使用者 ID
ja: ユーザー ID
ko: 사용자 ID
ru: Идентификатор пользователя
ms: ID pengguna
pt-br: ID do usuario
required: true
type: number
- default: 1000
edit: true
envKey: PGID
labelEn: Group ID
labelZh: 用户组 ID
label:
en: Group ID
zh: 用户组 ID
zh-Hant: 使用者群組 ID
ja: グループ ID
ko: 그룹 ID
ru: Идентификатор группы
ms: ID kumpulan
pt-br: ID do grupo
required: true
type: number
- default: 60
edit: true
envKey: POLLING_INTERVAL
labelEn: Polling Interval
labelZh: 检查间隔
label:
en: Polling Interval
zh: 检查间隔
zh-Hant: 檢查間隔
ja: 確認間隔
ko: 확인 간격
ru: Интервал проверки
ms: Selang semakan
pt-br: Intervalo de verificacao
required: true
type: number
- default: "true"
edit: true
envKey: BASIC_AUTH
labelEn: Enable Basic Authentication
labelZh: 启用基本认证
label:
en: Enable Basic Authentication
zh: 启用基本认证
zh-Hant: 啟用基本驗證
ja: Basic 認証を有効化
ko: 기본 인증 사용
ru: Включить базовую аутентификацию
ms: Dayakan pengesahan asas
pt-br: Ativar autenticacao basica
required: true
type: select
values:
- label: "true"
value: "true"
- label: "false"
value: "false"
- default: admin
edit: true
envKey: PODFETCH_USERNAME
labelEn: Administrator Username
labelZh: 管理员用户名
label:
en: Administrator Username
zh: 管理员用户名
zh-Hant: 管理員使用者名稱
ja: 管理者ユーザー名
ko: 관리자 사용자 이름
ru: Имя администратора
ms: Nama pengguna pentadbir
pt-br: Nome do administrador
required: true
type: text
- default: ""
edit: true
envKey: PODFETCH_PASSWORD
labelEn: Administrator Password
labelZh: 管理员密码
label:
en: Administrator Password
zh: 管理员密码
zh-Hant: 管理員密碼
ja: 管理者パスワード
ko: 관리자 비밀번호
ru: Пароль администратора
ms: Kata laluan pentadbir
pt-br: Senha do administrador
random: true
required: true
rule: paramComplexity
type: password
- default: ./data/podcasts
edit: true
envKey: PODCASTS_DIR
labelEn: Podcast Directory
labelZh: 播客目录
label:
en: Podcast Directory
zh: 播客目录
zh-Hant: Podcast 目錄
ja: ポッドキャストディレクトリ
ko: 팟캐스트 디렉터리
ru: Каталог подкастов
ms: Direktori podcast
pt-br: Diretorio de podcasts
required: true
type: text
- default: ./data/db
edit: true
envKey: DATABASE_DIR
labelEn: Database Directory
labelZh: 数据库目录
label:
en: Database Directory
zh: 数据库目录
zh-Hant: 資料庫目錄
ja: データベースディレクトリ
ko: 데이터베이스 디렉터리
ru: Каталог базы данных
ms: Direktori pangkalan data
pt-br: Diretorio do banco de dados
required: true
type: text
+27
View File
@@ -0,0 +1,27 @@
services:
podfetch:
image: "docker.io/samuel19982/podfetch:latest"
container_name: ${CONTAINER_NAME}
restart: unless-stopped
user: "${PUID}:${PGID}"
networks:
- 1panel-network
ports:
- "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8000"
environment:
- PORT=8000
- POLLING_INTERVAL=${POLLING_INTERVAL}
- DATABASE_URL=sqlite:///app/db/podcast.db
- PODFETCH_FOLDER=/app/podcasts
- BASIC_AUTH=${BASIC_AUTH}
- USERNAME=${PODFETCH_USERNAME}
- PASSWORD=${PODFETCH_PASSWORD}
volumes:
- "${PODCASTS_DIR}:/app/podcasts"
- "${DATABASE_DIR}:/app/db"
labels:
createdBy: Apps
networks:
1panel-network:
external: true
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}"
fail() {
printf '%s\n' "$1" >&2
exit 1
}
read_env_value() {
local key="$1"
local value=""
value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2-)"
case "$value" in
\"*\" | \'*\') value="${value:1:${#value}-2}" ;;
esac
printf '%s\n' "$value"
}
prepare_directory() {
local key="$1"
local raw="$2"
local absolute=""
[[ -n "$raw" ]] || fail "${key} must not be empty"
case "$raw" in
*$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) fail "${key} contains unsupported dotenv characters" ;;
esac
if [[ "$raw" = /* ]]; then
[[ ! -L "$raw" ]] || fail "${key} must not be a symbolic link"
absolute="$(realpath -m -- "$raw")"
[[ "$absolute" != "/" ]] || fail "${key} must not be the filesystem root"
if [[ -e "$absolute" ]]; then
[[ -d "$absolute" ]] || fail "${key} must be a directory"
[[ "$(stat -c '%u:%g' "$absolute")" == "${PUID}:${PGID}" ]] || fail "Existing ${key} must be owned by ${PUID}:${PGID}"
else
install -d -m 0750 -- "$absolute"
chown "${PUID}:${PGID}" -- "$absolute"
fi
else
absolute="$(realpath -m -- "${ROOT_DIR}/${raw#./}")"
case "$absolute" in
"${ROOT_DIR}"/*) ;;
*) fail "Relative ${key} must remain inside the application version directory" ;;
esac
[[ ! -L "$absolute" ]] || fail "${key} must not be a symbolic link"
install -d -m 0750 -- "$absolute"
chown "${PUID}:${PGID}" -- "$absolute"
fi
}
[[ "$(id -u)" -eq 0 ]] || fail "PodFetch init must run as root"
[[ -f "$ENV_FILE" ]] || fail "Environment file not found: ${ENV_FILE}"
[[ ! -L "$ENV_FILE" ]] || fail "Environment file must not be a symbolic link"
PUID="${PUID:-$(read_env_value PUID)}"
PGID="${PGID:-$(read_env_value PGID)}"
[[ "$PUID" =~ ^[0-9]+$ && "$PGID" =~ ^[0-9]+$ ]] || fail "PUID and PGID must be numeric"
prepare_directory PODCASTS_DIR "${PODCASTS_DIR:-$(read_env_value PODCASTS_DIR)}"
prepare_directory DATABASE_DIR "${DATABASE_DIR:-$(read_env_value DATABASE_DIR)}"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Podcast media and the SQLite database are retained for recovery.
exit 0
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# PodFetch applies its database migrations when the new container starts.
exit 0
+9
View File
@@ -0,0 +1,9 @@
PodFetch logo
Source: https://github.com/SamTV12345/PodFetch/blob/b28002a56bd0291df3a4b184cf5673dcd99aceec/ui/public/logo.png
Source commit: b28002a56bd0291df3a4b184cf5673dcd99aceec
Source SHA-256: 3ad03a34777eff296167d19f38f0084c54096f7b78e725958953d05d7e8d58c4
License: Apache License 2.0 (Apache-2.0)
License text: https://github.com/SamTV12345/PodFetch/blob/b28002a56bd0291df3a4b184cf5673dcd99aceec/LICENSE
The store asset is a proportional rendering of the official project logo.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+11
View File
@@ -0,0 +1,11 @@
CONTAINER_NAME=podfetch
PANEL_APP_BIND_ADDRESS=0.0.0.0
PANEL_APP_PORT_HTTP=40100
PUID=1000
PGID=1000
POLLING_INTERVAL=60
BASIC_AUTH=true
PODFETCH_USERNAME=admin
PODFETCH_PASSWORD=replace-with-a-strong-password
PODCASTS_DIR=./data/podcasts
DATABASE_DIR=./data/db
+170
View File
@@ -0,0 +1,170 @@
additionalProperties:
formFields:
- default: 0.0.0.0
edit: true
envKey: PANEL_APP_BIND_ADDRESS
labelEn: Bind Address
labelZh: 绑定地址
label:
en: Bind Address
zh: 绑定地址
zh-Hant: 綁定位址
ja: バインドアドレス
ko: 바인드 주소
ru: Адрес привязки
ms: Alamat ikatan
pt-br: Endereco de vinculacao
required: true
type: text
- default: 40100
edit: true
envKey: PANEL_APP_PORT_HTTP
labelEn: HTTP Port
labelZh: HTTP 端口
label:
en: HTTP Port
zh: HTTP 端口
zh-Hant: HTTP 連接埠
ja: HTTP ポート
ko: HTTP 포트
ru: HTTP-порт
ms: Port HTTP
pt-br: Porta HTTP
required: true
rule: paramPort
type: number
- default: 1000
edit: true
envKey: PUID
labelEn: User ID
labelZh: 用户 ID
label:
en: User ID
zh: 用户 ID
zh-Hant: 使用者 ID
ja: ユーザー ID
ko: 사용자 ID
ru: Идентификатор пользователя
ms: ID pengguna
pt-br: ID do usuario
required: true
type: number
- default: 1000
edit: true
envKey: PGID
labelEn: Group ID
labelZh: 用户组 ID
label:
en: Group ID
zh: 用户组 ID
zh-Hant: 使用者群組 ID
ja: グループ ID
ko: 그룹 ID
ru: Идентификатор группы
ms: ID kumpulan
pt-br: ID do grupo
required: true
type: number
- default: 60
edit: true
envKey: POLLING_INTERVAL
labelEn: Polling Interval
labelZh: 检查间隔
label:
en: Polling Interval
zh: 检查间隔
zh-Hant: 檢查間隔
ja: 確認間隔
ko: 확인 간격
ru: Интервал проверки
ms: Selang semakan
pt-br: Intervalo de verificacao
required: true
type: number
- default: "true"
edit: true
envKey: BASIC_AUTH
labelEn: Enable Basic Authentication
labelZh: 启用基本认证
label:
en: Enable Basic Authentication
zh: 启用基本认证
zh-Hant: 啟用基本驗證
ja: Basic 認証を有効化
ko: 기본 인증 사용
ru: Включить базовую аутентификацию
ms: Dayakan pengesahan asas
pt-br: Ativar autenticacao basica
required: true
type: select
values:
- label: "true"
value: "true"
- label: "false"
value: "false"
- default: admin
edit: true
envKey: PODFETCH_USERNAME
labelEn: Administrator Username
labelZh: 管理员用户名
label:
en: Administrator Username
zh: 管理员用户名
zh-Hant: 管理員使用者名稱
ja: 管理者ユーザー名
ko: 관리자 사용자 이름
ru: Имя администратора
ms: Nama pengguna pentadbir
pt-br: Nome do administrador
required: true
type: text
- default: ""
edit: true
envKey: PODFETCH_PASSWORD
labelEn: Administrator Password
labelZh: 管理员密码
label:
en: Administrator Password
zh: 管理员密码
zh-Hant: 管理員密碼
ja: 管理者パスワード
ko: 관리자 비밀번호
ru: Пароль администратора
ms: Kata laluan pentadbir
pt-br: Senha do administrador
random: true
required: true
rule: paramComplexity
type: password
- default: ./data/podcasts
edit: true
envKey: PODCASTS_DIR
labelEn: Podcast Directory
labelZh: 播客目录
label:
en: Podcast Directory
zh: 播客目录
zh-Hant: Podcast 目錄
ja: ポッドキャストディレクトリ
ko: 팟캐스트 디렉터리
ru: Каталог подкастов
ms: Direktori podcast
pt-br: Diretorio de podcasts
required: true
type: text
- default: ./data/db
edit: true
envKey: DATABASE_DIR
labelEn: Database Directory
labelZh: 数据库目录
label:
en: Database Directory
zh: 数据库目录
zh-Hant: 資料庫目錄
ja: データベースディレクトリ
ko: 데이터베이스 디렉터리
ru: Каталог базы данных
ms: Direktori pangkalan data
pt-br: Diretorio do banco de dados
required: true
type: text
+27
View File
@@ -0,0 +1,27 @@
services:
podfetch:
image: "docker.io/samuel19982/podfetch:v5.2.2@sha256:d8b8e546d3ae40c6e7821343c3d0f0a238b47cb772295e95c7f8683ca419663f"
container_name: ${CONTAINER_NAME}
restart: unless-stopped
user: "${PUID}:${PGID}"
networks:
- 1panel-network
ports:
- "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8000"
environment:
- PORT=8000
- POLLING_INTERVAL=${POLLING_INTERVAL}
- DATABASE_URL=sqlite:///app/db/podcast.db
- PODFETCH_FOLDER=/app/podcasts
- BASIC_AUTH=${BASIC_AUTH}
- USERNAME=${PODFETCH_USERNAME}
- PASSWORD=${PODFETCH_PASSWORD}
volumes:
- "${PODCASTS_DIR}:/app/podcasts"
- "${DATABASE_DIR}:/app/db"
labels:
createdBy: Apps
networks:
1panel-network:
external: true
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}"
fail() {
printf '%s\n' "$1" >&2
exit 1
}
read_env_value() {
local key="$1"
local value=""
value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2-)"
case "$value" in
\"*\" | \'*\') value="${value:1:${#value}-2}" ;;
esac
printf '%s\n' "$value"
}
prepare_directory() {
local key="$1"
local raw="$2"
local absolute=""
[[ -n "$raw" ]] || fail "${key} must not be empty"
case "$raw" in
*$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) fail "${key} contains unsupported dotenv characters" ;;
esac
if [[ "$raw" = /* ]]; then
[[ ! -L "$raw" ]] || fail "${key} must not be a symbolic link"
absolute="$(realpath -m -- "$raw")"
[[ "$absolute" != "/" ]] || fail "${key} must not be the filesystem root"
if [[ -e "$absolute" ]]; then
[[ -d "$absolute" ]] || fail "${key} must be a directory"
[[ "$(stat -c '%u:%g' "$absolute")" == "${PUID}:${PGID}" ]] || fail "Existing ${key} must be owned by ${PUID}:${PGID}"
else
install -d -m 0750 -- "$absolute"
chown "${PUID}:${PGID}" -- "$absolute"
fi
else
absolute="$(realpath -m -- "${ROOT_DIR}/${raw#./}")"
case "$absolute" in
"${ROOT_DIR}"/*) ;;
*) fail "Relative ${key} must remain inside the application version directory" ;;
esac
[[ ! -L "$absolute" ]] || fail "${key} must not be a symbolic link"
install -d -m 0750 -- "$absolute"
chown "${PUID}:${PGID}" -- "$absolute"
fi
}
[[ "$(id -u)" -eq 0 ]] || fail "PodFetch init must run as root"
[[ -f "$ENV_FILE" ]] || fail "Environment file not found: ${ENV_FILE}"
[[ ! -L "$ENV_FILE" ]] || fail "Environment file must not be a symbolic link"
PUID="${PUID:-$(read_env_value PUID)}"
PGID="${PGID:-$(read_env_value PGID)}"
[[ "$PUID" =~ ^[0-9]+$ && "$PGID" =~ ^[0-9]+$ ]] || fail "PUID and PGID must be numeric"
prepare_directory PODCASTS_DIR "${PODCASTS_DIR:-$(read_env_value PODCASTS_DIR)}"
prepare_directory DATABASE_DIR "${DATABASE_DIR:-$(read_env_value DATABASE_DIR)}"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Podcast media and the SQLite database are retained for recovery.
exit 0
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# PodFetch applies its database migrations when the new container starts.
exit 0