<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: yim wang (enigmatic man)</title>
    <description>The latest articles on DEV Community by yim wang (enigmatic man) (@dongyan_huangenigmatic).</description>
    <link>https://dev.to/dongyan_huangenigmatic</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4110752%2F5a26f6ff-2ba4-4b07-9af4-1d473304d12f.png</url>
      <title>DEV Community: yim wang (enigmatic man)</title>
      <link>https://dev.to/dongyan_huangenigmatic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dongyan_huangenigmatic"/>
    <language>en</language>
    <item>
      <title>TiviMate vs Smarters vs GSE vs iMPlayer — Which Live TV App Holds Up in 2026</title>
      <dc:creator>yim wang (enigmatic man)</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:43:12 +0000</pubDate>
      <link>https://dev.to/dongyan_huangenigmatic/tivimate-vs-smarters-vs-gse-vs-implayer-which-live-tv-app-holds-up-in-2026-9p0</link>
      <guid>https://dev.to/dongyan_huangenigmatic/tivimate-vs-smarters-vs-gse-vs-implayer-which-live-tv-app-holds-up-in-2026-9p0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Test window: 2026-08 to 2026-09, hardware: NVIDIA Shield TV, Mi TV box, iPhone, Android phone, ~200 Mbps line&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The player is the easy part. What people underestimate is that &lt;strong&gt;the playlist and EPG feed you feed it matter more than the app&lt;/strong&gt;. After three weeks running the same ~200-channel M3U through five apps, the practical answers are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android TV / Fire TV box&lt;/strong&gt;: TiviMate. No real contest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed household (phones + tablets + TV)&lt;/strong&gt;: Smarters Pro&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero budget, clean UI&lt;/strong&gt;: GSE Smart IPTV&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple TV + iPhone combo&lt;/strong&gt;: iMPlayer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already living in Kodi&lt;/strong&gt;: stay in Kodi, the migration cost isn't worth it for most people&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Details and test data below.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The mental model first (for people new to this)
&lt;/h2&gt;

&lt;p&gt;A live streaming app like TiviMate or Smarters is a &lt;strong&gt;player, not a content provider&lt;/strong&gt;. You supply two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;playlist&lt;/strong&gt; (M3U URL, or an Xtream Codes login) â€” this is &lt;em&gt;what channels exist&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;EPG feed&lt;/strong&gt; (XMLTV) â€” this is &lt;em&gt;what's on when&lt;/em&gt;, i.e. the guide&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Citable definition&lt;/strong&gt;: An M3U playlist is a plain-text media manifest, one channel per entry, each pairing a name with a stream URL; the UTF-8 variant (m3u8) is what HLS deployments use. Xtream Codes is an API-based alternative: server URL + username + password, which typically loads faster on low-memory hardware than a big M3U file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In one line: &lt;strong&gt;the app is the shell, the playlist is the food, the EPG is the menu.&lt;/strong&gt; Get all three right or the experience collapses.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What I actually tested
&lt;/h2&gt;

&lt;p&gt;Same ~200-channel M3U + one XMLTV EPG, fed to each app on the same hardware, three weeks of daily use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Player&lt;/th&gt;
&lt;th&gt;Platforms&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Load 200 ch&lt;/th&gt;
&lt;th&gt;EPG handling&lt;/th&gt;
&lt;th&gt;Multi-playlist&lt;/th&gt;
&lt;th&gt;4K HEVC hw decode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TiviMate&lt;/td&gt;
&lt;td&gt;Android TV, Fire TV&lt;/td&gt;
&lt;td&gt;free, ~$5/yr premium&lt;/td&gt;
&lt;td&gt;~12 s&lt;/td&gt;
&lt;td&gt;Local cache, instant&lt;/td&gt;
&lt;td&gt;Premium&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smarters Pro&lt;/td&gt;
&lt;td&gt;Android, iOS, PC, TV&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;td&gt;~40 s&lt;/td&gt;
&lt;td&gt;Fetched live, stutters on big XMLTV&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (ExoPlayer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSE Smart IPTV&lt;/td&gt;
&lt;td&gt;Everywhere&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;td&gt;~35 s&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (premium tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iMPlayer&lt;/td&gt;
&lt;td&gt;Android, iOS, Apple TV&lt;/td&gt;
&lt;td&gt;~$20 buyout&lt;/td&gt;
&lt;td&gt;~20 s&lt;/td&gt;
&lt;td&gt;Web dashboard syncs EPG across devices&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TVBox (CN mod ecosystem)&lt;/td&gt;
&lt;td&gt;Android boxes&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;td&gt;instant&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Many groups&lt;/td&gt;
&lt;td&gt;Depends on the box&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few findings that don't show up in review articles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Channel-switch latency differs more than you'd think.&lt;/strong&gt; TiviMate averaged 1.8 s from button-press to video. Smarters Pro on a 4K source sat on a black screen for 3-5 s first. On a 1 GB-RAM box, Smarters' menus visibly lagged; TiviMate stayed smooth. That gap is the difference between an app &lt;em&gt;designed for&lt;/em&gt; a TV box and one that just &lt;em&gt;runs on&lt;/em&gt; one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EPG is where everything breaks.&lt;/strong&gt; The same XMLTV file: TiviMate caches it locally and the guide opens instantly; Smarters refetches it, and with a few hundred channels you'll see the spinner. iMPlayer's killer feature is its web dashboard â€” you manage playlists, EPG, and per-device hiding rules from one browser, which is genuinely the best multi-device setup I've used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playlist decay is the real enemy, not the player.&lt;/strong&gt; A month-old public playlist lost roughly &lt;strong&gt;25-35% of its entries&lt;/strong&gt; when re-tested (September 2026; sample via iptv-search.com). Most failures were expired auth tokens or dead segment URLs; the rest were upstream takedowns. No player fixes this â€” you need a batch health check on the playlist itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. The part nobody's comparison article covers: maintaining the playlist
&lt;/h2&gt;

&lt;p&gt;The player choice is a one-hour decision. Keeping a playlist &lt;em&gt;alive&lt;/em&gt; is a recurring chore, and that's where the workflow I use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Import the m3u/txt into a batch tester&lt;/li&gt;
&lt;li&gt;Four-layer check per entry â€” playlist reachable, live signature (MEDIA-SEQUENCE advancing), newest 2-3 segments return 200, then a 3-second throughput sample for smoothness&lt;/li&gt;
&lt;li&gt;Export only the surviving channels back to M3U&lt;/li&gt;
&lt;li&gt;Re-import into the player&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I run this with a small open-source desktop tool, &lt;strong&gt;goplay-source/IPTV-tools&lt;/strong&gt; on GitHub (search "IPTV-tools"). It does the batch test with 1-50 worker threads, scores smoothness, resolves source IPs offline via the ip2region database, filters out expired-token and ad-placeholder entries, and exports a clean M3U you drop straight into TiviMate or Smarters. Full cycle on a 200-entry list: under 10 minutes. No Python install required â€” there's a packaged Windows exe. I'm involved in maintaining it, so treat that as a disclosure; the code is all in the repo if you want to audit the logic. If you only maintain one or two playlists, a few &lt;code&gt;curl&lt;/code&gt; commands is honestly enough â€” don't install anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Pick by your setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shield / Mi box / Fire TV â†’ TiviMate.&lt;/strong&gt; Sideload the APK (not on Play for TV builds). Playlist + EPG URL, done. Premium unlocks recording and multi-playlist, ~$5/yr.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iPhone in the house â†’ Smarters Pro or iMPlayer.&lt;/strong&gt; Same Xtream account works everywhere with Smarters. iMPlayer if you want the cross-device dashboard; $20 buyout for two devices is cheaper than most subscriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free is fine â†’ GSE Smart IPTV.&lt;/strong&gt; Clean, no ads, occasional EPG hiccups on very large XMLTV files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kodi already there â†’ PVR IPTV Simple Client + M3U.&lt;/strong&gt; Kodi's decoder compatibility is the widest (H.265, fMP4, edge-case DRM). Config is the trade-off; not a beginner path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chinese Android-box TVBox ecosystem â†’ TVBox.&lt;/strong&gt; Dead-simple setup, but there's no built-in health check, which is exactly why section 3 exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does the same M3U play on my phone but not my TV box?&lt;/strong&gt;&lt;br&gt;
A: Usually the codec. 4K HEVC needs hardware decode, and not every box has it. Check the box's decoder first, then the source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need an EPG?&lt;/strong&gt;&lt;br&gt;
A: Not to watch. But without one you're blind-switching channels, no catch-up, no per-program favorites. Keep the M3U and XMLTV from the same provider â€” mismatched channel names give you an empty guide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does 4K stutter on my "fast" connection?&lt;/strong&gt;&lt;br&gt;
A: Live streams don't buffer like VOD. If the node/link can't sustain ~50 Mbps for 4K (or ~30 Mbps for 1080p), it stalls within seconds. Test with a 3-second throughput sample, not a speedtest average.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: My playlist died. Do I need to find everything again?&lt;/strong&gt;&lt;br&gt;
A: No â€” batch-test the existing list first. In my samples, a 30-day-old list still had 65-75% of entries alive. Export the survivors; only hunt for replacements on the dead channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is this legal?&lt;/strong&gt;&lt;br&gt;
A: Players are legal and content-agnostic. Whether &lt;em&gt;your&lt;/em&gt; playlist's content is legal is up to the provider you chose. Use public/open lists at your own discretion and check the terms of your provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;TiviMate: tivimate.com (premium $4.99/yr, verified 2026-09)&lt;/li&gt;
&lt;li&gt;RFC 8216: HTTP Live Streaming (IETF, 2017)&lt;/li&gt;
&lt;li&gt;ip2region: github.com/jinfangzhua/ip2region&lt;/li&gt;
&lt;li&gt;Test sample data: iptv-search.com (2026-09)&lt;/li&gt;
&lt;li&gt;Batch-testing tooling: goplay-source/IPTV-tools (GitHub, 2026-09)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>streaming</category>
      <category>tv</category>
    </item>
    <item>
      <title>Why IPTV Streams Keep Dying: A Technical Deep-Dive into m3u8 Protocols and Automated Maintenance</title>
      <dc:creator>yim wang (enigmatic man)</dc:creator>
      <pubDate>Thu, 10 Sep 2026 10:41:07 +0000</pubDate>
      <link>https://dev.to/dongyan_huangenigmatic/why-iptv-streams-keep-dying-a-technical-deep-dive-into-m3u8-protocols-and-automated-maintenance-j01</link>
      <guid>https://dev.to/dongyan_huangenigmatic/why-iptv-streams-keep-dying-a-technical-deep-dive-into-m3u8-protocols-and-automated-maintenance-j01</guid>
      <description>&lt;h1&gt;
  
  
  Why IPTV Streams Keep Dying: A Technical Deep-Dive into m3u8 Protocols and Automated Maintenance
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Short Answer
&lt;/h2&gt;

&lt;p&gt;IPTV sources don't fail because of "poor quality." They fail due to &lt;strong&gt;fundamental protocol design constraints&lt;/strong&gt;. Understanding why is the only way to build a system that actually stays working.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why Do Live Streams Expire?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 Multicast vs Unicast: The Overseas Problem
&lt;/h3&gt;

&lt;p&gt;IPTV services from Chinese carriers (China Telecom, China Unicom, China Mobile) use &lt;strong&gt;IGMP multicast&lt;/strong&gt; at the network layer.&lt;/p&gt;

&lt;p&gt;Multicast works like this: the stream is sent once, and all subscribers in the same multicast group share it. The cost is low. The problem is also fundamental — &lt;strong&gt;multicast packets cannot cross router boundaries&lt;/strong&gt;. Once you leave the carrier's internal network, multicast addresses simply stop working.&lt;/p&gt;

&lt;p&gt;This is why IPTV sources that work perfectly in mainland China fail completely behind a VPN. It's not a bandwidth issue. It's a protocol limitation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workaround&lt;/strong&gt;: convert multicast to unicast. This requires a router that supports IGMP proxy (OpenWrt + igmpproxy), or capturing the unicast URL via packet sniffing from inside the LAN.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Tokens and Timestamps: The Expiration Problem
&lt;/h3&gt;

&lt;p&gt;Many stream URLs look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://example.com/live/channel.m3u8?token=abc123&amp;amp;expiry=1725...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token is dynamically generated and expires. This pattern appears in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Carrier-internal sources (anti-piracy)&lt;/li&gt;
&lt;li&gt;Paid subscription sources (per-view billing)&lt;/li&gt;
&lt;li&gt;Free transcoding sites (bandwidth protection)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to identify&lt;/strong&gt;: URLs containing &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;expires&lt;/code&gt;, or &lt;code&gt;t=&lt;/code&gt; parameters are almost certainly time-limited.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3 Server Load and Anti-Scraping
&lt;/h3&gt;

&lt;p&gt;Public free m3u8 lists are usually backed by personal servers or cheap VPS. Three things go wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth exhaustion&lt;/strong&gt;: A popular channel with hundreds of concurrent viewers causes the server to 503&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referer checking&lt;/strong&gt;: The source validates the request header and rejects non-whitelisted domains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP rate limiting&lt;/strong&gt;: Too many requests from one IP triggers a ban&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This explains why the same source plays fine in a browser but fails in a player — the User-Agent differs, and the source treats them differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.4 Copyright and Moderation
&lt;/h3&gt;

&lt;p&gt;No point dwelling on this. CCTV, satellite channels, sports events — any complaint and the source disappears. This factor is entirely outside your control. Accept it and build redundancy.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. m3u8 Format Deep-Dive
&lt;/h2&gt;

&lt;p&gt;You can't write a good checker without understanding the format.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 M3U Playlist Structure
&lt;/h3&gt;

&lt;p&gt;A standard M3U file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXTM3U
#EXTINF:-1 tvg-id="CCTV1.cn" tvg-name="CCTV-1" tvg-logo="https://..." group-title="CCTV",CCTV-1
http://example.com/live/cctv1.m3u8
#EXTINF:-1 tvg-id="HKPH.ts" tvg-name="凤凰资讯" group-title="HK-Macau-Taiwan",凤凰资讯
http://example.com/live/phoenix.m3u8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#EXTINF&lt;/code&gt;: Channel metadata. Value before comma = duration (-1 means live). After comma = channel name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tvg-id&lt;/code&gt;: Unique channel identifier for EPG matching&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;group-title&lt;/code&gt;: Group tag for categorization&lt;/li&gt;
&lt;li&gt;URL line: The actual stream address&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.2 VOD Playlist vs Live Playlist
&lt;/h3&gt;

&lt;p&gt;Same &lt;code&gt;.m3u8&lt;/code&gt; extension, completely different things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VOD (Video on Demand)&lt;/strong&gt;: Contains all segments, plays to completion&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
segment0.ts
#EXTINF:10.0,
segment1.ts
#EXT-X-ENDLIST
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Live&lt;/strong&gt;: Continuously updated, no &lt;code&gt;#EXT-X-ENDLIST&lt;/code&gt; marker&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:1847
#EXTINF:6.0,
segment1847.ts
#EXTINF:6.0,
segment1848.ts
#EXT-X-ENDLIST  ← presence means VOD, absence means live
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Detection key&lt;/strong&gt;: After fetching the m3u8 manifest, if there's no &lt;code&gt;#EXT-X-ENDLIST&lt;/code&gt; and &lt;code&gt;MEDIA-SEQUENCE&lt;/code&gt; is incrementing, it's a real live stream. Many "live sources" are just VOD files with a misleading extension.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 HLS Segment Window
&lt;/h3&gt;

&lt;p&gt;Live streams retain only the most recent N segments (typically 3-10). Older segments are deleted by the server. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The m3u8 URL you have is only valid while segments still exist on the server&lt;/li&gt;
&lt;li&gt;If a source goes 24 hours without viewers, segments may be purged, and a fresh request returns 404&lt;/li&gt;
&lt;li&gt;This explains why "a source that worked yesterday is dead today"&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Batch Detection: Technical Approach
&lt;/h2&gt;

&lt;p&gt;Given the failure modes above, a detection strategy needs three layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 The Three-Layer Model
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1: Connectivity check (HTTP HEAD / GET request)
    ↓ passes
Layer 2: Manifest parsing (m3u8 header analysis)
    ↓ passes
Layer 3: Stream quality (actual segment download, speed + codec analysis)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Layer 1&lt;/strong&gt; answers "is the source alive?", &lt;strong&gt;Layer 2&lt;/strong&gt; answers "is this actually live or just VOD?", &lt;strong&gt;Layer 3&lt;/strong&gt; answers "does it play smoothly?".&lt;/p&gt;

&lt;p&gt;Most tools stop at Layer 1, which is why their output lists still contain many dead sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Layer 1: Connectivity Check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_redirects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_time_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;HEAD&lt;/code&gt; not &lt;code&gt;GET&lt;/code&gt; to save bandwidth&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;allow_redirects=True&lt;/code&gt; — many sources 302-redirect&lt;/li&gt;
&lt;li&gt;Timeout of 5 seconds is sufficient to determine alive/dead&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; for concurrency (10-20 threads depending on network)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.3 Layer 2: Live Stream Detection
&lt;/h3&gt;

&lt;p&gt;Connectivity passing doesn't mean it's live. Parse the m3u8 manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze_m3u8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;

        &lt;span class="c1"&gt;# #EXT-X-ENDLIST means VOD, not live
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#EXT-X-ENDLIST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_live&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;seq_match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#EXT-X-MEDIA-SEQUENCE:(\d+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seq_match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seq_match&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

        &lt;span class="n"&gt;duration_match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#EXT-X-TARGETDURATION:(\d+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;target_duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration_match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;duration_match&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;live&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_live&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sequence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;target_duration&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advanced approach (more accurate): fetch the manifest twice with a few seconds apart, compare whether &lt;code&gt;MEDIA-SEQUENCE&lt;/code&gt; incremented. Incrementing = live. Unchanged = VOD or dead.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 Layer 3: Speed Testing
&lt;/h3&gt;

&lt;p&gt;Download the first segment and measure actual throughput:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_speed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Download first m3u8 segment, compute throughput and score.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
        &lt;span class="n"&gt;segment_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;segment_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;segment_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;bytes_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;bytes_read&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;speed_kbps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_read&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;speed_kbps&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;excellent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;speed_kbps&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;good&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;speed_kbps&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;poor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speed_kbps&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.5 Complete Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input: M3U playlist file
  ↓
Parse: extract all channel names + URLs
  ↓
Layer 1: Concurrent HEAD requests, filter out 4xx/5xx/timeout
  ↓
Layer 2: Parse m3u8 headers for passed URLs, filter out VOD
  ↓
Layer 3: 3-second speed test on live streams, output scoring
  ↓
Output: Structured results with status and scores (JSON/CSV/M3U)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Automated Maintenance
&lt;/h2&gt;

&lt;p&gt;Manual checking doesn't scale. Sources live for days, not months. You need automation.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Periodic Re-checking
&lt;/h3&gt;

&lt;p&gt;Simplest approach: cron job running the check script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run at 3 AM daily (lowest server load)&lt;/span&gt;
0 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; python check_iptv.py &lt;span class="nt"&gt;--input&lt;/span&gt; sources.m3u &lt;span class="nt"&gt;--output&lt;/span&gt; checked.m3u
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After each run, diff against the previous result. Keep only sources that are still valid. Export the new list.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Multi-Source Redundancy
&lt;/h3&gt;

&lt;p&gt;Keep 2-3 sources per channel. Primary fails → automatic failover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXTINF:-1 group-title="CCTV",CCTV-1 (primary)
http://source-a.com/live/cctv1.m3u8
#EXTINF:-1 group-title="CCTV",CCTV-1 (backup-1)
http://source-b.com/cctv1.m3u8
#EXTINF:-1 group-title="CCTV",CCTV-1 (backup-2)
http://source-c.com/cctv1_live.m3u8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Players try in order. Primary 404s → auto-switch to backup. TVBox and Kodi both support this.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Geolocation Filtering
&lt;/h3&gt;

&lt;p&gt;Overseas users should prefer servers closest to them. Use an offline IP geolocation database like ip2region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ip2region&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Ip2region&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Ip2region&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip2region.xdb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;103.XX.XX.XX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Returns: China|Guangdong|Guangzhou|China Telecom
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tag sources by region. Prefer Hong Kong, Singapore, US nodes for overseas viewers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 Blacklist Maintenance
&lt;/h3&gt;

&lt;p&gt;Add known low-quality source domains to a blacklist, skip detection entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# blacklist.txt
&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;-&lt;span class="n"&gt;source&lt;/span&gt;.&lt;span class="n"&gt;xyz&lt;/span&gt;
&lt;span class="n"&gt;iptv&lt;/span&gt;-&lt;span class="n"&gt;live&lt;/span&gt;.&lt;span class="n"&gt;top&lt;/span&gt;
&lt;span class="n"&gt;old&lt;/span&gt;-&lt;span class="n"&gt;cdn&lt;/span&gt;.&lt;span class="n"&gt;net&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Existing Open Source Tools
&lt;/h2&gt;

&lt;p&gt;The community has several mature detection tools, each with different strengths:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Notable Features&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/flyfishes/IPTV-M3U-Checker2" rel="noopener noreferrer"&gt;IPTV-M3U-Checker2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;DingTalk/WeChat bot notifications, Excel preview&lt;/td&gt;
&lt;td&gt;Server users needing alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/peterpt/IPTV-CHECK" rel="noopener noreferrer"&gt;IPTV-CHECK&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;GUI + CLI, OCR validation&lt;/td&gt;
&lt;td&gt;Desktop users, visual needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/kristofferR/IPTVChecker" rel="noopener noreferrer"&gt;IPTVChecker-Python&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Screenshots, bitrate analysis, retry backoff&lt;/td&gt;
&lt;td&gt;Deep analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/Guovin/iptv-api" rel="noopener noreferrer"&gt;iptv-api&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Full desktop app, EPG support&lt;/td&gt;
&lt;td&gt;Ready-to-use solution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/tolgakurtuluss/mytv" rel="noopener noreferrer"&gt;mytv (xdxd)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Multi-source merge/dedup, strict HLS validation&lt;/td&gt;
&lt;td&gt;Building playlists from scratch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/goplay-source/IPTV-tools" rel="noopener noreferrer"&gt;IPTV-tools&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Batch checking + live detection + IP geolocation, Desktop GUI, sort/filter results&lt;/td&gt;
&lt;td&gt;Lightweight rapid validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://iptv-search.com" rel="noopener noreferrer"&gt;iptv-search.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;Aggregated source search by channel/region/category&lt;/td&gt;
&lt;td&gt;Source discovery phase&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  6. FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does it work in VLC but not in TVBox?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: VLC's default timeout is long (~30s). TVBox defaults to 5s. Many slow sources play in VLC but time out in TVBox. Try increasing TVBox's timeout to 10-15s in settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I tell if a source is multicast or unicast?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Check the IP in the URL. &lt;code&gt;239.x.x.x&lt;/code&gt; is a multicast address (Class D IP). These sources die outside the carrier LAN. Normal unicast sources use regular public IPs or domain names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: My check script returns only 10% valid sources. Is that normal?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Yes. Public m3u8 lists have an average source lifespan of 1-2 weeks. Losing 30-50% weekly is normal. The key is re-running regularly to keep the list fresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I set up my own IPTV source server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Technically yes. You'd need a server with sufficient bandwidth + RTMP/HLS transcoding + source scraping scripts. But copyright risk and legal risk in China need careful consideration — there are clear regulations around IPTV retransmission.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Maintaining IPTV streams isn't a "find one good source" problem. It's a &lt;strong&gt;continuous verification + multi-source redundancy + periodic update&lt;/strong&gt; engineering problem.&lt;/p&gt;

&lt;p&gt;Core methodology:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Understand failure modes (multicast limits, token expiry, load throttling)&lt;/li&gt;
&lt;li&gt;Build a three-layer detection mechanism (connectivity → live detection → quality scoring)&lt;/li&gt;
&lt;li&gt;Automate periodic re-checking&lt;/li&gt;
&lt;li&gt;Maintain multi-source redundancy to reduce single-point failures&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tools are just instruments. The mindset matters more. Once you understand the mechanics, the specific tool you use becomes almost irrelevant.&lt;/p&gt;

</description>
      <category>iptv</category>
      <category>python</category>
      <category>streaming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Live TV Search Tool After M3U Links Died</title>
      <dc:creator>yim wang (enigmatic man)</dc:creator>
      <pubDate>Sat, 05 Sep 2026 06:36:53 +0000</pubDate>
      <link>https://dev.to/dongyan_huangenigmatic/how-i-built-a-live-tv-search-tool-after-m3u-links-died-3j6o</link>
      <guid>https://dev.to/dongyan_huangenigmatic/how-i-built-a-live-tv-search-tool-after-m3u-links-died-3j6o</guid>
      <description>&lt;p&gt;Last year, my mom called me from back home saying she could not watch the Spring Festival Gala on TV. The IPTV source she had been using for months had just stopped working — one of those free M3U playlists that expire after a few weeks.&lt;/p&gt;

&lt;p&gt;I tried to help her remotely, but the whole process was frustrating. I found a playlist on CSDN, imported it into her player, and half the channels were dead. Tried another one from GitHub — same thing. It felt like whack-a-mole.&lt;/p&gt;

&lt;p&gt;Honestly, I thought there had to be a better way. This is a problem plenty of overseas Chinese people face, and yet every solution I found was either outdated or paid.&lt;/p&gt;

&lt;p&gt;So I built something myself.&lt;/p&gt;

&lt;p&gt;I wrote a script that crawls public streaming sources every morning, tests which URLs actually work, and keeps only the healthy ones. Then I built a simple search page where you can type a channel name and get a working stream URL instantly — no need to browse through hundreds of lines in a text file.&lt;/p&gt;

&lt;p&gt;The tech stack is pretty minimal: Node.js on Cloudflare Workers, D1 for the database, and a daily cron job to refresh the source list. Nothing fancy, just something that works.&lt;/p&gt;

&lt;p&gt;I shared it with a few friends overseas and word spread. Turns out a lot of people were tired of the same M3U headaches. Now the tool supports searching by channel name, country, and category, and covers Chinese, Korean, Japanese, and European channels.&lt;/p&gt;

&lt;p&gt;If you have ever struggled with dead streams or expired playlists, you might find it useful. It is free and open source. Happy to answer any questions in the comments.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>python</category>
      <category>streaming</category>
    </item>
  </channel>
</rss>
