# yuanzl77/IPTV: an aggregator with an empty source list

> yuanzl77/IPTV is a Python script that fetches the m3u and txt URLs you list, filters them with an aiohttp sweep and an ffprobe pass, and writes a sorted live.m3u with EPG. The shipped config has no source URLs in it, so the aggregation happens only after you supply your own.

**yuanzl77/IPTV** — Automatically updates IPTV live streaming sources daily, supporting IPv4/IPv6 dual stack. Custom channels, high-quality streams, no ads.

- Repository: https://github.com/yuanzl77/IPTV
- Website: https://yuanzl77.github.io
- Stars: 2,133 · Forks: 309
- Language: Python
- License: Apache-2.0
- Published: 2026-10-14 · Updated: 2026-10-14 · Language: en
- Canonical page: https://hysenlabs.com/projects/yuanzl77-iptv

## Why the aggregator starts with nothing to aggregate

Everything the project automates sits behind one list that the repository leaves empty. `source_urls` in config.py is where the pipeline gets its work, and the shipped value is an empty list. The README is blunt about the consequence: the tool supplies only a technical framework, and the person running it has to bring legal data sources, so a fresh clone produces no playlist at all. That is worth holding next to the repository description, which advertises daily automatic updates of live sources gathered from across the web, and next to the topic list, which carries `iptv-free`. The aggregation is real as code and absent as content, so the first session goes into filling a config list rather than importing channels.

Who it is for follows from that. A viewer who wants something to watch tonight ends up with an empty playlist. A viewer who already owns sources, a self-built relay or a public feed they are authorised to redistribute, gets the part of the job that otherwise eats an evening: fetch every source, drop the dead ones, sort what survives and republish it on a schedule. Compliance travels with the job, and the README puts responsibility for authorised use of every URL on whoever runs the copy.

The rest of the shape is small and plain: Python, default branch main, and top-level `check.py`, `config.py`, `main.py`, `requirements.txt`, `demo.txt`, `fetch_hotel.py`, a `.gitignore`, an `image/` folder, a `.github/` folder and a committed `__pycache__/` directory, with a homepage at https://yuanzl77.github.io. There are no GitHub releases and no tags, so upgrades arrive by pulling main, whose last push was on 2026-09-21 under an Apache-2.0 licence.

## Exact and fuzzy names decide which channels survive the merge

The merge is name based, and the README describes both halves of it: channels are collected from multiple sources, then combined by exact channel name match and by fuzzy match. Two feeds that name the same channel differently therefore land near each other, and the fuzzy pass catches the naming drift that hand-built lists accumulate over months.

Output arrives as two files in the project root. `live.m3u` is the M3U format for players that want EPG data and channel logos, and `live.txt` is the flat TXT format that TVBox and Fengying read directly. Both carry the same merged set, so the player decides which file you point at rather than which channels you see.

Quality gets written into the entry text instead of staying implicit. Each output URL has resolution and bitrate appended, in the form 【1920x1080@256kbps】, a shape that makes the generated file self-describing before a stream is opened. Sorting has 2 levels: anything that cleared the ffprobe pass goes ahead of the rest, and within a tier entries fall in descending bitrate then resolution, so the entries worth trying float to the top of live.m3u.

A second gap deserves naming. The feature list says the M3U header carries several built-in EPG addresses for TiviMate and Kodi, yet the config file it tells you to edit shows `epg_urls` empty too, next to an empty `announcements` and an empty `url_blacklist`. Either the shipped config differs from the README excerpt or those values are injected somewhere the README does not name. Open the header of your own generated file and confirm the EPG addresses are really there before trusting guide data.

## Two quality gates stand between a URL and the playlist

The quality pass has 2 stages, and both run before an entry reaches live.m3u. Stage one is an aiohttp concurrent pull of each playlist that establishes reachability. Stage two is an ffprobe probe that opens the stream and pulls out resolution, bitrate and codec information. The first answers whether the URL answers at all, the second answers whether what comes back is watchable, and the sort order is built from the second stage's verdict.

Patience and concurrency are hand-set numbers in config.py, and the three lines that drive the pass look like this:

```python
# 质量检测
enable_quality_check = True    # True=启用质量检测（测活后过滤失效源），False=直接输出不过滤
check_timeout    = 3.5         # 单个 URL HTTP 请求超时时间（秒），超时视为失效
check_max_conn   = 50          # 最大并发检测数，调高可加速但更占带宽
```

`check_timeout` gives a single URL 3.5 seconds, and a timeout counts as dead. `check_max_conn` caps simultaneous checks at 50, and the comment on that line says a higher cap runs faster while taking more bandwidth, so the exchange is wall-clock time against network load rather than accuracy. Address family is the third switch: `ip_version_priority` takes `ipv6` or `ipv4` and decides which family the generated URLs prefer. The README backs the IPv6 case with lower latency and less buffering, better multicast support, and steadier connections because no address translation sits between the two families. Whether your network cooperates is not something the script can work out, and the README points at `test-ipv6.com` for that check.

## Disabling the quality check republishes every dead URL

`enable_quality_check` is the switch that matters most, because its false branch does not mean weaker checking. The comment states that true filters dead sources after the reachability pass and that false writes the output with no filtering at all. Set it to false and neither the aiohttp sweep nor the ffprobe probe runs, so live.m3u carries every URL you listed, including the ones that time out on a good day. You also lose the resolution and bitrate values ffprobe produced, which means the annotations disappear and the two sort tiers collapse into one, since nothing is left to rank them by.

The blacklist is manual by design. After each run the script prints the domains that failed repeatedly, and `url_blacklist` filters any address containing one of its substrings, but nothing writes those printed suggestions into the file for you. Every run therefore ends with a human editing config.py, a cost the feature list does not mention. Over-filtering has its own price, since blacklist matching is substring based and a broad entry quietly drops more of your own sources along with the offender.

Read together, the constants describe a pipeline tuned for a modest list of fast sources. A relay that answers in 4 seconds looks exactly like a dead one at the 3.5 second timeout, and a list long enough to fill 50 concurrent slots loses more entries to bandwidth contention than to genuine failure. Nothing in config.py separates a slow source from a broken one, so the repair when entries vanish is to raise the timeout and lower the cap, then read the next run's log before deciding which of the two you got wrong.

## Editing config.py and running main.py locally

The local path is a short sequence. Clone the repository, install the two dependencies, put your own authorised URLs into `source_urls`, and run the entry point:

```bash
git clone https://github.com/yuanzl77/IPTV.git
cd IPTV
pip install -r requirements.txt
python main.py
```

The dependency list holds 2 packages and nothing else: requests at 2.28.0 or newer and aiohttp at 3.8.0 or newer, Both are floors rather than pins. ffprobe is in neither list, so the probe stage needs that binary on your machine before main.py gets far.

Two config blocks drive everything else:

```python
# IP 优先级：ipv6 或 ipv4
ip_version_priority = "ipv6"

# 数据源列表（支持 m3u / txt 格式）
source_urls = [ ... ]

# URL 黑名单（包含任意子串的地址会被过滤）
url_blacklist = [ ... ]
```

```python
# 公告条目（写在直播源头部）
announcements = [ ... ]

# EPG 地址列表
epg_urls = [ ... ]
```

Three files land in the project root afterwards: `live.m3u`, `live.txt` and `function.log`, with the log carrying the blacklist suggestions. For a daily refresh the README points at cron on Linux and macOS, or Task Scheduler on Windows, to run `python main.py` again each day. Look at what the clone actually holds before editing: check.py, fetch_hotel.py and demo.txt sit there with no explanation anywhere in the README, and a `__pycache__/` directory is committed beside them, which is build residue left in version control. Read those three files yourself rather than assuming main.py is the whole pipeline.

## Forking to let GitHub Actions rebuild the playlist daily

The fork path exists so the rebuild happens without a machine left switched on. Fork the repository, edit `source_urls` in your copy, then open the Actions tab and accept the prompt reading I understand my workflows, go ahead and enable them. Workflows do not run on a fresh fork until that prompt is accepted, so until then the scheduled job produces nothing. Triggering one run by hand is the quickest way to learn whether live.m3u is generated the way you expect.

Schedule and output both live outside the Python. The scheduled task runs every day at 06:00 Beijing time, and the generated `live.m3u` and `live.txt` appear in the repository root, or in a configured output directory, where a raw link feeds a player directly. That timing and the output location are defined in the `.github/workflows` directory, and the README does not reproduce those files, so changing the run time or the branch means editing the workflow in your own fork. A local run and a scheduled run can therefore diverge quietly in how often they fire and where they write.

Running builds inside someone else's account carries consequences the README spells out. A fork is an independent copy the author cannot control, and responsibility for authorised use of every URL stays with whoever runs it. Practically, when a scheduled build fails on a fork, the only person who can edit the workflow, the config and the blacklist is the fork's owner, so have an answer ready before the 06:00 run rather than after it.

## The Docker image sits under a different account name

Container support exists as one pull command, which is the shortest path to a run if Docker is already there. The account name is the surprise: the GitHub repository is yuanzl77/IPTV while the published image carries a different string.

```bash
docker pull yuanzl7712/iptv:latest
```

That command is the whole Docker section. There is no run example, no volume for config.py, no port mapping, and nothing about where live.m3u appears inside the container, so a containerised setup means guessing at all four. The section is labelled as being in internal beta, and the comment on the pull line says the image can be removed at any time and is for learning and exchange only, which is a thin guarantee for something you would schedule daily. Treat it as a preview rather than a deployment target: the local run with cron, or the Actions schedule you control, has documented behaviour behind it, while the image has a pull line and a disclaimer. Neither the digest behind the latest tag nor its update cadence is stated, so a rebuild can change under you between pulls.

## What a live checker does differently from this aggregator

The one alternative the project points at is iptv-checker, hosted at https://github.com/zhimin-dev/iptv-checker, suggested for playback quality in a personal environment. The difference in approach is the point. yuanzl77/IPTV reaches outward: it fetches the sources you list, probes them concurrently, merges by channel name and republishes the result, so your player consumes a file produced by a scheduled build. A live checker starts from the stream you are already trying to play and reports whether your own setup handles it, which leaves the playlist alone. If you already maintain a curated m3u by hand and only want to know which entries work on your network, that second job is the one you have. If you want the list rebuilt every morning from several remote sources without opening an editor, the first job is the one you have, and nothing in this repository replaces it.

Both jobs leave the same question with you. The checks here are reachability plus ffprobe metadata, so the script can tell you that a stream opens and reports a resolution, a bitrate and a codec. It cannot tell you whether a channel is one you are entitled to watch, and nothing in config.py, in the workflow or in the two output files addresses that.

## Conclusion

Adopt yuanzl77/IPTV if you already hold authorised streams and want a scheduled merge job instead of a hand-edited list, and skip the clone if you were expecting channels to arrive with it. Fill source_urls first, then read the header of one generated live.m3u to confirm the EPG addresses are there, because the shipped epg_urls list is empty. When entries vanish between runs, raise check_timeout, lower check_max_conn and compare function.log before blaming the source, and leave the Docker image alone until it documents a run command.

## FAQ

### What does yuanzl77/IPTV need before it produces a playlist?

You have to fill source_urls in config.py with your own authorised m3u or txt URLs, because the shipped list is empty and the README states that the tool provides only a technical framework. After that, python main.py writes live.m3u, live.txt and function.log to the project root.

### Does yuanzl77/IPTV ship any channels of its own?

No default playlist, m3u file or channel list comes with the repository. The description advertises daily aggregation of live sources, but the configuration you clone has no entries in source_urls, so the output only ever reflects URLs you added yourself.

### How do I run yuanzl77/IPTV every day without leaving a computer on?

Fork the repository, edit source_urls in your copy, then accept the GitHub Actions prompt so the workflows run. The scheduled task fires every day at 06:00 Beijing time and writes live.m3u and live.txt into the repository root, where a raw link can be imported into a player.

### What happens if I set enable_quality_check to False in yuanzl77/IPTV?

The output is written with no filtering, so neither the aiohttp reachability sweep nor the ffprobe probe runs and dead URLs reach live.m3u. You also lose the resolution and bitrate values used for the entry annotations and for sorting.

### Which output file should I use for Kodi, TiviMate or TVBox?

Use live.m3u for M3U players such as Kodi and TiviMate, since it carries EPG data and channel logos, and use live.txt for TVBox and Fengying, which read the flat TXT format directly. Both files hold the same merged channel set.

## Sources

- [Issues](https://github.com/yuanzl77/IPTV/issues)
- [License: Apache-2.0](https://github.com/yuanzl77/IPTV/blob/main/LICENSE)
- [Project website](https://yuanzl77.github.io)
- [README](https://github.com/yuanzl77/IPTV/blob/main/README.md)
- [yuanzl77/IPTV on GitHub](https://github.com/yuanzl77/IPTV)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/yuanzl77-iptv
