linksift README

The project's README from github.com/loveisbl1nd/linksift. As one markdown file: README.md.

LinkSift

LinkSift

Turn links into a tidy local queue.
Inspect, format, and save media from a focused self-hosted web workspace.

CI status Latest release GHCR architectures MIT License Powered by yt-dlp Docker runtime

LinkSift is a local-first media downloader powered by yt-dlp and ffmpeg. Paste one or more supported URLs, inspect the available metadata, choose MP4 or MP3, and follow each download from the same queue.

Built for personal, authorized use. Respect copyright law, platform terms, and creators' rights. LinkSift does not support DRM circumvention or bypassing access controls.

At a glance

Deployment Versioned GHCR image for normal use; source build and local launcher for contributors
Interface Responsive browser UI with light, dark, and system themes
Formats MP4 video or MP3 audio
Queue Multiple URLs, quality selection, concurrency limit, live progress
Runtime Python + Flask, yt-dlp, ffmpeg, Gunicorn, non-root container
Privacy model Local by default; no built-in account, telemetry, or public service

Screenshots

LinkSift light theme desktop workspace LinkSift dark theme desktop workspace
Light theme - desktop workspace Dark theme - desktop workspace
LinkSift light theme mobile workspace LinkSift dark theme mobile workspace
Light theme - mobile Dark theme - mobile

The workflow

01 - Inspect

Paste a URL or a batch of URLs. LinkSift asks yt-dlp for metadata without downloading the media first.
02 - Choose

Pick MP4 or MP3, then select an available video quality when the source provides one.
03 - Collect

Watch progress, speed, and ETA. Save completed files through the browser or an optional folder picker.

What is included

Quick start

Docker is the supported end-user path. Install Docker Desktop, then start the published image:

docker run -d --name linksift --restart unless-stopped -p 127.0.0.1:8899:8899 -v linksift-downloads:/app/downloads ghcr.io/loveisbl1nd/linksift:latest

Open http://localhost:8899. You do not need Python, yt-dlp, ffmpeg, or a virtual environment on the host.

Downloads persist in the named linksift-downloads Docker volume. Pin a numbered image such as 0.1.0 instead of latest when reproducibility matters. Stop and remove the container with docker stop linksift followed by docker rm linksift; the volume remains intact.

To use Compose with the published image after cloning the repository:

docker compose -f compose.ghcr.yml up -d

To build the current source locally instead, run docker compose up --build -d.

Development

The local launcher is for contributors and requires Python 3.12, yt-dlp, and ffmpeg:

./linksift.sh

Before opening a pull request, run:

python -m unittest discover -s tests -v
python -m py_compile app.py
docker compose config
docker compose -f compose.ghcr.yml config
docker build -t linksift:local .

See CONTRIBUTING.md for the contributor workflow and pull request checklist.

Releases and image verification

Pushing a tag in the form vMAJOR.MINOR.PATCH runs the release pipeline. It repeats the offline validation suite, builds linux/amd64 and linux/arm64 images, publishes SemVer and latest tags to GHCR, attaches supply-chain metadata, and creates the matching GitHub Release.

After installing the GitHub CLI, verify that a published image was built by this repository's release workflow:

gh attestation verify oci://ghcr.io/loveisbl1nd/linksift:0.2.0 -R loveisbl1nd/linksift

Maintainers should follow RELEASING.md, including the one-time GHCR visibility check. An attestation establishes build origin; it does not replace source or dependency review.

Configuration

Variable Default Purpose
PORT 8899 HTTP port used by the development server.
HOST 127.0.0.1 Bind address. Keep it local unless a protected reverse proxy is in front.
LINKSIFT_DOWNLOAD_TIMEOUT 3600 Maximum seconds allowed for one yt-dlp process.
LINKSIFT_MAX_CONCURRENT_DOWNLOADS 3 Number of download worker slots — jobs actually running at the same time, clamped to 1–16. Jobs beyond this limit wait in a FIFO queue instead of being rejected. Invalid, zero, or negative values fall back to the default.
LINKSIFT_MAX_QUEUED_DOWNLOADS 200 Maximum jobs allowed to wait in the queue, not counting running jobs. When the queue is full, POST /api/download returns 429. Invalid, zero, or negative values fall back to the default.
LINKSIFT_CONCURRENT_FRAGMENTS 4 Fragment parallelism inside a single DASH/HLS download (yt-dlp --concurrent-fragments), clamped to 1–16. Invalid, zero, or negative values fall back to the default.
LINKSIFT_JOB_TTL 86400 Seconds a terminal job (done, error, timed_out, or cancelled) and its files are kept before automatic cleanup. Invalid, zero, or negative values fall back to the default.
LINKSIFT_MAX_PLAYLIST_ITEMS 200 Maximum playlist entries expanded per inspection. Longer playlists are truncated to the first N items. Invalid values fall back to the default.
LINKSIFT_JOB_RETRIES 2 Extra fresh-extraction attempts after a failed download attempt (0–5). Only transient failures (HTTP 403/429/5xx, network resets, timeouts) are retried. Invalid values fall back to the default.
LINKSIFT_RETRY_BASE_DELAY 2 Seconds before the first retry; doubles per retry and is capped at 15 s. Backoff time counts against LINKSIFT_DOWNLOAD_TIMEOUT. Invalid or negative values fall back to the default.
LINKSIFT_PO_TOKEN_PROVIDER_URL unset Base URL of a bgutil PO token provider (robust mode). Must be an http/https URL with a hostname; invalid values are ignored with a warning.
LINKSIFT_NO_UPDATE unset Set to 1 to skip the startup update of yt-dlp and yt-dlp-ejs.

LINKSIFT_MAX_CONCURRENT_DOWNLOADS controls how many downloads run at once; LINKSIFT_MAX_QUEUED_DOWNLOADS controls how many may wait behind them. Queued jobs report status: "queued" and a 1-based queue_position from GET /api/status/<job_id>, and can be cancelled before they start. LINKSIFT_CONCURRENT_FRAGMENTS only parallelizes fragmented (DASH/HLS) downloads — it does not speed up every URL, and higher concurrency values increase CPU and network load without guaranteeing faster downloads. Total resource use scales with both settings combined: up to LINKSIFT_MAX_CONCURRENT_DOWNLOADS × LINKSIFT_CONCURRENT_FRAGMENTS fragment connections plus one ffmpeg process per running job can be active at the same time.

Job state is held in memory. The Docker command therefore uses one Gunicorn worker; restarting the service or container clears queued and active job state, and partially downloaded .part files are not resumed automatically after a restart (the TTL cleanup removes them instead). This is accepted behavior for the local-first design. Do not add workers until job state moves to shared storage.

Downloaded files and job status are a temporary cache, not an archive: LinkSift removes finished jobs and their files after LINKSIFT_JOB_TTL seconds and sweeps stale leftover files it created at startup and periodically while running. Active downloads are never touched by TTL cleanup. Save completed files through the browser before the TTL expires. Playlists larger than LINKSIFT_MAX_PLAYLIST_ITEMS only queue the first configured number of items; truncation is detected from the playlist size reported by yt-dlp, and unavailable or malformed playlist entries are skipped without failing the request.

Multi-output downloads (v0.3)

Multi-output is available in LinkSift v0.3.0. One inspected URL can produce multiple explicit outputs (for example MP4 at a chosen height plus an MP3). The request becomes a single parent job with one artifact per output (a000, a001, …). Artifacts run sequentially under one shared deadline; each completed output is individually saveable while the others continue.

Overview

Using the UI

  1. Paste a URL and let LinkSift inspect it (metadata is fetched without downloading media first).
  2. Choose the outputs you want: MP4 at one or more heights, MP3 audio, or any combination. When you select both video and audio, LinkSift extracts audio from the downloaded video with ffmpeg, saving bandwidth and time.
  3. Follow progress per artifact. Each card shows phase, speed, ETA, and percent. Save individual completed outputs through the browser or an optional folder picker while the rest are still running.

API: legacy single-output request

The original format / format_id fields still work and produce a single-artifact parent for backward compatibility:

POST /api/download
{
  "url": "https://example.com/watch?v=...",
  "format": "audio"
}

API: multi-output request

Send an outputs array. Each entry is an object with type (video or audio). A video entry also requires a format_id (the yt-dlp format id, e.g. 137 for 1080p MP4). Audio entries omit format_id.

POST /api/download
{
  "url": "https://example.com/watch?v=...",
  "outputs": [
    {"type": "video", "format_id": "137"},
    {"type": "audio"}
  ]
}

outputs and the legacy fields are mutually exclusive; mixing them returns a 400 error. Duplicate (type, format_id) pairs are rejected.

Status response

GET /api/status/<job_id> returns the parent plus an artifacts array with per-output progress. For multi-output jobs the response includes artifacts and current_artifact_id:

{
  "status": "downloading",
  "phase": "downloading",
  "percent": 45.0,
  "downloaded_bytes": 1048576,
  "total_bytes": 8388608,
  "speed": 524288,
  "eta": 14,
  "attempt": 1,
  "max_attempts": 3,
  "current_artifact_id": "a001",
  "artifacts": [
    {
      "id": "a000", "type": "video", "format_id": "137",
      "status": "done", "phase": "done", "percent": 100.0,
      "filename": "Title (MP4 137).mp4", "attempt": 1, "max_attempts": 3
    },
    {
      "id": "a001", "type": "audio", "format_id": null,
      "status": "downloading", "phase": "downloading", "percent": 40.0,
      "downloaded_bytes": 419430, "total_bytes": 1048576,
      "speed": 524288, "eta": 1, "attempt": 1, "max_attempts": 3
    }
  ]
}

Note that the parent's status stays downloading for the whole active run; the finer-grained stage is carried by phase, which mirrors the current artifact's active phase (see Parent phases while active). Terminal parent phases are never overwritten.

Parent job statuses

Status Meaning
queued Waiting for a free worker slot. Reports a 1-based queue_position.
downloading The pipeline is running. Covers everything from claiming the job to the last artifact finishing; the current stage is reported by phase, not by status.
cancelling Cancellation was requested while running; the owning worker is terminating the subprocess and finalizing.
done Every artifact completed.
partial Some artifacts completed and some failed; completed outputs remain saveable.
error No artifact completed; the run failed.
cancelled The job was cancelled before or during the run.
timed_out The shared deadline expired before completion.

starting is a phase, not a parent status. A worker that has just claimed a job sets status: "downloading" together with phase: "starting"; there is no parent status: "starting".

Parent phases while active

While the parent is active (status: "downloading"), its phase mirrors the phase of the artifact currently running, so the UI can distinguish the stages within one download:

Phase Meaning
starting A worker claimed the job, or the next artifact's process is being spawned.
downloading The current artifact is actively transferring bytes.
retrying A transient failure on the current artifact is being retried.
processing Postprocessing (muxing/extraction) is running for the current artifact.
postprocessing Accepted for forward compatibility; the current pipeline reports this stage as processing.

Terminal parent phases are never overwritten: once a parent is done, partial, error, cancelled, or timed_out, a stale artifact phase cannot resurrect it as active.

Artifact statuses and phases

Status / phase Meaning
pending Artifact has not started yet.
starting yt-dlp/ffmpeg process is being spawned for this artifact.
downloading The artifact is actively downloading.
retrying A transient download failure is being retried.
processing Postprocessing (muxing/extraction) is running.
done The artifact finished and is saveable.
error The artifact failed permanently.
cancelled The artifact was cancelled.
timed_out The artifact did not finish before the deadline.

An artifact keeps status: "downloading" for the whole time it is active; its phase carries the finer-grained stage. The frontend renders each artifact from its own status/phase pair, so a completed artifact shows ✓ Complete with a Save button while a failed one shows ✗ Failed with none.

File endpoints

Video-to-audio reuse

When a request selects both video and audio, the pipeline runs video artifacts first. An audio artifact then attempts to extract its MP3 from an already-downloaded MP4 using ffmpeg (try_ffmpeg_reuse) instead of launching a fresh yt-dlp audio download. This avoids a second network fetch.

A reuse attempt has exactly three outcomes, and they are kept strictly distinct:

Outcome What happens Falls back to a yt-dlp audio download?
Success The converted file is published atomically and the artifact becomes done. Not needed
Ordinary failure ffmpeg exits non-zero, produces no output file, or the final publish (rename) fails with OSError. try_ffmpeg_reuse returns False and logs a warning. Yes — the caller downloads the audio with yt-dlp, provided the parent is still active and time remains
Cancellation or deadline expiry The call raises: PipelineCancelled for a stop request, subprocess.TimeoutExpired when the parent's shared deadline is exhausted. The parent finalizes as cancelled or timed_out. No — never

Only the ordinary-failure branch falls back. Not every publication error is fatal, and not every fatal outcome is a publication error: what makes cancellation and deadline expiry fatal is that spawning a fresh download for a job the user already stopped, or whose time budget is already spent, is exactly the bug this separation prevents. When a stop request and an expired deadline are both true, cancellation wins and the parent reports cancelled.

Shared parent deadline

All artifacts in a parent share a single deadline computed once at pipeline start (time.monotonic() + LINKSIFT_DOWNLOAD_TIMEOUT). Each artifact checks the remaining budget before it begins; if the

linksift (loveisbl1nd/linksift)

Searching Database…

这是什么?LinkSift 是一个本地优先、自托管的网页应用,让你粘贴任何 yt-dlp 支持的 URL,查看其元数据,并将其加入队列,使用 ffmpeg 下载为 MP4 视频或 MP3 音频。
这是桌面应用还是我自己运行的服务器?它是你用 Docker 运行、通过响应式浏览器界面使用的自托管服务,不是原生桌面应用,也不是公开托管服务。
怎么把它跑起来?安装 Docker Desktop 并启动已发布的 GHCR 镜像;默认情况下,Compose 配置只绑定到 localhost 的 8899 端口。
需要我自己安装 Python、yt-dlp 或 ffmpeg 吗?不需要——Docker 镜像捆绑了 Python、yt-dlp、ffmpeg 和 Gunicorn,所以唯一真正的要求就是 Docker。
下载的文件会永久保存吗?不会,下载只是临时缓存,不是存储:已完成的任务及其文件会在可配置的 TTL 之后被删除,重启容器会清空内存中的队列,且不会恢复未完成的文件。
LinkSift 需要账户或向别处发送数据吗?不需要,它没有内置账户系统、遥测或公共服务组件——默认就是本地的。
能从同一个 URL 同时获取视频和音频吗?可以,从 v0.3 起,一次请求可以产生多个输出(例如 MP4 加 MP3);LinkSift 会先下载视频,再用 ffmpeg 从中提取 MP3,而不是单独抓取音频。
Docker 镜像支持哪些 CPU 架构?带版本号的发布同时提供 amd64 和 arm64 镜像,并附带 OCI 元数据、SBOM 和 GitHub 构建来源证明。
LinkSift 是用来绕过 DRM 或付费墙的吗?不是,它明确为个人授权使用而构建,作者声明它不支持绕过 DRM 或访问控制。
多个下载能以全速并行运行吗?并发由 LINKSIFT_MAX_CONCURRENT_DOWNLOADS 和 LINKSIFT_CONCURRENT_FRAGMENTS 限制,而且应用只运行一个带有内存任务状态的 Gunicorn worker,因此它围绕单一共享队列设计,而不是横向扩展。
Size
1.9 MB
Stars
123
Age
2mo
Commits · 90 Days