Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .cursor/worktrees.json

This file was deleted.

11 changes: 9 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,20 @@ ADMIN_PASSWORD=your_admin_password
# Set a key to allow remote access with authentication.
#TMS_API_KEY=your_secret_api_key

# POST /api/v1/downloads accepts {"url":"..."} or {"torrent_base64":"<standard base64 of .torrent>"} (mutually exclusive).

# Webhook URL for download completion/failure notifications (optional)
# TMS POSTs JSON with id, title, status (completed|failed|stopped), error, event_id
# For OpenClaw: use gateway hooks URL, e.g. http://127.0.0.1:18789/hooks/tms (same host)
# Default JSON: id, title, status (completed|failed|stopped), error, event_id — use with OpenClaw hooks.mappings
# OpenClaw /hooks/wake expects {"text","mode"} — if URL contains /hooks/wake or /hooks/agent, TMS adapts automatically
# Or set TMS_WEBHOOK_FORMAT: tms | json | openclaw_wake | openclaw_agent
#TMS_WEBHOOK_URL=http://127.0.0.1:18789/hooks/tms
#TMS_WEBHOOK_FORMAT=
# Token sent as Authorization: Bearer (required for OpenClaw hooks). Generate: openssl rand -hex 32
# Must match OpenClaw hooks.token exactly (401 on mismatch).
#TMS_WEBHOOK_TOKEN=

# From Docker, TMS_WEBHOOK_URL cannot use 127.0.0.1 to reach OpenClaw on the host — use host.docker.internal (Desktop) or extra_hosts + host-gateway (Linux).

# Update yt-dlp on application start (default: true)
# Runs yt-dlp -U once at startup (does not block if update fails)
# Path to yt-dlp binary (default: /usr/bin/yt-dlp, standard on Arch).
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.24'
go-version: '1.26'

- name: Cache Go modules
uses: actions/cache@v5
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-1.24-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-1.26-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-1.24-
${{ runner.os }}-go-1.26-

- name: Download dependencies
run: go mod download
Expand All @@ -39,7 +39,7 @@ jobs:
- name: Lint
uses: golangci/golangci-lint-action@v9.2.0
with:
version: v2.10.1
version-file: .golangci-lint-version
args: --timeout 5m

- name: Vet
Expand Down Expand Up @@ -75,17 +75,17 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.24'
go-version: '1.26'

- name: Cache Go modules
uses: actions/cache@v5
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-1.24-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-1.26-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-1.24-
${{ runner.os }}-go-1.26-

- name: Download dependencies
run: go mod download
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ src/
pkg/
.vscode/
build/
.idea/

.DS_Store

Expand All @@ -50,6 +51,10 @@ gosec-report.json
# Binary files
telegram-media-server
*.torrent
# Integration fixture (keep in git despite *.torrent above)
!integration/fixtures/big-buck-bunny.torrent

.mypy_cache/
logs.txt

.cursor/
1 change: 1 addition & 0 deletions .golangci-lint-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.11.4
34 changes: 33 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ VERSION=$(shell git describe --tags --always --dirty)
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
LDFLAGS=-ldflags "-X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}"

# Last log lines: make logs LOG_LINES=50
LOG_LINES ?= 100
SYSTEMD_UNIT ?= telegram-media-server
COMPOSE_SERVICE ?= telegram-media-server

# Dependency checks
.PHONY: check-deps
check-deps:
Expand Down Expand Up @@ -114,7 +119,17 @@ format:
go run golang.org/x/tools/cmd/goimports@latest -w .
go mod tidy

GOLANGCI_LINT_VERSION ?= v2.10.1
GOLANGCI_LINT_VERSION_FILE := .golangci-lint-version
GOLANGCI_LINT_SEMVER := $(shell tr -d '\n\r ' < $(GOLANGCI_LINT_VERSION_FILE) 2>/dev/null | sed 's/^v//')
GOLANGCI_LINT_VERSION ?= v$(GOLANGCI_LINT_SEMVER)

.PHONY: update-golangci-lint-version
update-golangci-lint-version:
@python3 -c 'import json, urllib.request; \
r = json.load(urllib.request.urlopen("https://api.github.com/repos/golangci/golangci-lint/releases/latest")); \
tag = r["tag_name"]; \
open(".golangci-lint-version", "w").write(tag[1:] + "\n" if tag.startswith("v") else tag + "\n")'
@echo "Wrote golangci-lint $$(tr -d '\n\r ' < $(GOLANGCI_LINT_VERSION_FILE)) to $(GOLANGCI_LINT_VERSION_FILE)"

.PHONY: lint
lint:
Expand Down Expand Up @@ -156,6 +171,7 @@ test-integration:
go test -v -run Integration ./internal/handlers/session
go test -v -run Integration ./internal/filemanager
go test -v -run "TestValidateContentIntegration" ./internal/downloader/torrent
go test -tags=integration -v ./internal/api/ -run TestAPI_AddDownload_201TorrentBase64

.PHONY: test-docker
test-docker: docker-test-build
Expand Down Expand Up @@ -211,6 +227,21 @@ restart:
@echo "Restarting telegram-media-server..."
systemctl restart telegram-media-server

# Recent service logs: systemd journal if available, otherwise Docker Compose (see LOG_LINES).
.PHONY: logs
logs:
@if command -v journalctl >/dev/null 2>&1; then \
journalctl -u $(SYSTEMD_UNIT) -n $(LOG_LINES) --no-pager; \
elif docker compose version >/dev/null 2>&1; then \
docker compose logs --tail=$(LOG_LINES) $(COMPOSE_SERVICE); \
elif command -v docker-compose >/dev/null 2>&1; then \
docker-compose logs --tail=$(LOG_LINES) $(COMPOSE_SERVICE); \
else \
echo "Neither journalctl nor docker compose found. On a host install: use journalctl after system install,"; \
echo "or Docker Compose from the project directory. For 'make run-local', logs go to that terminal."; \
exit 1; \
fi

# Docker test commands
.PHONY: docker-test-build
docker-test-build:
Expand Down Expand Up @@ -315,6 +346,7 @@ help:
@echo "Service Management:"
@echo " status - Check service status"
@echo " restart - Restart service"
@echo " logs - Last LOG_LINES log lines (journalctl or docker compose; default LOG_LINES=100)"
@echo ""
@echo "Docker:"
@echo " run - Run with Docker Compose"
Expand Down
1 change: 1 addition & 0 deletions cmd/telegram-media-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func main() {
}

app.ResumeIncompleteDownloads(a)
downloadManager.ResumePendingTVConversions(context.Background())

var apiServer *api.Server
if config.TMSAPIEnabled {
Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/NikitaDmitryuk/telegram-media-server

go 1.24.0
go 1.26

require (
github.com/go-bittorrent/magneturi v0.1.0
Expand All @@ -9,14 +9,14 @@ require (
github.com/google/uuid v1.6.0
github.com/jackpal/bencode-go v1.0.2
github.com/nicksnyder/go-i18n/v2 v2.6.1
golang.org/x/text v0.34.0
golang.org/x/text v0.35.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)

require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.33 // indirect
golang.org/x/net v0.49.0 // indirect
github.com/mattn/go-sqlite3 v1.14.37 // indirect
golang.org/x/net v0.52.0 // indirect
)
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -26,10 +26,10 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
Expand Down
Binary file added integration/fixtures/big-buck-bunny.torrent
Binary file not shown.
91 changes: 91 additions & 0 deletions integration/tms-api-add-download.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Integration helper: POST TMS /api/v1/downloads (not part of unit test suite).
#
# Default: sends integration/fixtures/big-buck-bunny.torrent as torrent_base64 (works with TMS in Docker).
#
# Usage (from repo root):
# ./integration/tms-api-add-download.sh
# ./integration/tms-api-add-download.sh /path/to/file.torrent
# ./integration/tms-api-add-download.sh /path/to/file.torrent 'Display title'
# ./integration/tms-api-add-download.sh 'https://example.com/video.mp4'
# TMS_BASE_URL=http://127.0.0.1:8080 TMS_API_KEY=secret ./integration/tms-api-add-download.sh
#
# Env:
# TMS_BASE_URL — API base (no trailing slash), default http://127.0.0.1:8080
# TMS_API_KEY — optional; sent as Authorization: Bearer
# TMS_TEST_TORRENT — override default .torrent path

set -euo pipefail

BASE="${TMS_BASE_URL:-http://127.0.0.1:8080}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DEFAULT_TORRENT="${TMS_TEST_TORRENT:-$SCRIPT_DIR/fixtures/big-buck-bunny.torrent}"

if ! command -v curl >/dev/null 2>&1; then
echo "error: curl is required" >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "error: python3 is required (for JSON body)" >&2
exit 1
fi

MODE="torrent"
TORRENT_FILE=""
VIDEO_URL=""
OPT_TITLE="${2:-}"

if [[ -n "${1:-}" ]]; then
if [[ -f "$1" ]]; then
TORRENT_FILE="$1"
MODE="torrent"
else
VIDEO_URL="$1"
MODE="url"
OPT_TITLE="${2:-}"
fi
else
TORRENT_FILE="$DEFAULT_TORRENT"
fi

if [[ "$MODE" == "torrent" ]]; then
if [[ ! -f "$TORRENT_FILE" ]]; then
echo "error: torrent file not found: $TORRENT_FILE" >&2
exit 1
fi
BODY=$(python3 -c '
import base64, json, pathlib, sys
path = pathlib.Path(sys.argv[1])
raw = path.read_bytes()
req = {"torrent_base64": base64.standard_b64encode(raw).decode("ascii")}
if len(sys.argv) > 2 and sys.argv[2]:
req["title"] = sys.argv[2]
print(json.dumps(req))
' "$TORRENT_FILE" "$OPT_TITLE")
else
if [[ -n "$OPT_TITLE" ]]; then
BODY=$(python3 -c 'import json,sys; print(json.dumps({"url": sys.argv[1], "title": sys.argv[2]}))' "$VIDEO_URL" "$OPT_TITLE")
else
BODY=$(python3 -c 'import json,sys; print(json.dumps({"url": sys.argv[1]}))' "$VIDEO_URL")
fi
fi

CURL_ARGS=(
-sS
-w "\n\nhttp_status:%{http_code}\n"
-X POST
"${BASE}/api/v1/downloads"
-H "Content-Type: application/json"
-d "$BODY"
)
if [[ -n "${TMS_API_KEY:-}" ]]; then
CURL_ARGS+=(-H "Authorization: Bearer ${TMS_API_KEY}")
fi

echo "POST ${BASE}/api/v1/downloads" >&2
if [[ "$MODE" == "torrent" ]]; then
echo "torrent_base64 from: $TORRENT_FILE" >&2
else
echo "url: ${VIDEO_URL}" >&2
fi
curl "${CURL_ARGS[@]}"
Loading
Loading