Compare commits

...

4 Commits

Author SHA1 Message Date
r3414385@gmail.com e2625265e7
Merge 5d52a557f4 into 43b8dced11 2026-03-08 20:22:12 +01:00
KAMI 43b8dced11
Add tvg-country attribute to M3U8 playlist entries (#990)
Add ISO 3166-1 alpha-2 country codes via tvg-country attribute
to all #EXTINF lines, enabling IPTV players to filter and display
channels by country.

Co-authored-by: Kálmán „KAMI” Szalai <kami911gmail.com>
2026-03-08 09:29:23 +01:00
KAMI 8a67a85d1a
Improve make_playlist.py code quality and fix bugs (#989)
* Improve make_playlist.py code quality and fix bugs

- Fix group name bug: replace underscores with spaces before title()
  so filenames like north_korea.md produce "North Korea" not "North_Korea"
- Fix resource leaks: use context managers for all file handles including
  EPG list and per-country playlist files
- Remove os.chdir() global side effect: use absolute paths derived from
  the script location instead
- Avoid calling to_m3u_line() twice per channel by caching the result
- Fix redundant trailing colon in filename[:-3:] slice
- Consistent use of write() for headers instead of mixing print/write
- Strip blank lines from EPG URL list when reading

* Skip commit and push when playlist has no changes

Prevents the workflow from failing with exit code 1 when the generated
playlist is identical to the previous run and there is nothing to commit.

* Use current branch instead of hardcoded master for push

Replace hardcoded origin/master reference with @{u} (upstream of current
branch) for the diff check, and use HEAD for the push target so the
workflow works correctly on any branch.

---------

Co-authored-by: Kálmán „KAMI” Szalai <kami911gmail.com>
2026-03-08 07:40:47 +01:00
r3414385@gmail.com 5d52a557f4
Update azerbaijan.md
HD
2025-12-30 10:39:46 +04:00
87 changed files with 3406 additions and 3309 deletions

View File

@ -17,5 +17,5 @@ jobs:
git config user.email "playlistbot@users.noreply.github.com" || true
python3 ./make_playlist.py
git add .
git commit --quiet -m "Update Playlist (GitHub Actions)"
git push -f origin master
git diff --staged --quiet || git commit --quiet -m "Update Playlist (GitHub Actions)"
git diff --quiet HEAD @{u} || git push -f origin HEAD

View File

@ -8,23 +8,23 @@
| # | Channel | Link | Logo | EPG id |
|:---:|:--------------:|:-----:|:----:|:------:|
| 0 | ARB 24 | [>](http://85.132.81.184:8080/arb/live/index.m3u8) | <img height="20" src="https://i.imgur.com/mtvIFyq.png"/> | ARB24.az |
| 0 | ARB Günəş Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=ARMGunes) | <img height="20" src="https://i.imgur.com/dSg7KUK.png"/> | ArbGunes.az |
| 0 | ARB Ⓢ | [>](http://109.205.166.68/server124/arb/index.m3u8) | <img height="20" src="https://i.imgur.com/E97M2OL.png"/> | ARB.az |
| 0 | Azad TV Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=ATVAz) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/ATV_%282012-h.h.%29.png/474px-ATV_%282012-h.h.%29.png"/> | AzadTV.az |
| 0 | ARB24 HD | [>](http://85.132.81.184:8080/arb/live/index.m3u8) | <img height="20" src="https://i.imgur.com/mtvIFyq.png"/> | ARB24.az |
| 0 | ARB Gunes HD Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=ARMGunes) | <img height="20" src="https://i.imgur.com/dSg7KUK.png"/> | ArbGunes.az |
| 0 | ARB HD Ⓢ | [>](http://109.205.166.68/server124/arb/index.m3u8) | <img height="20" src="https://i.imgur.com/E97M2OL.png"/> | ARB.az |
| 0 | ATV Azerbaijan HD Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=ATVAz) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/ATV_%282012-h.h.%29.png/474px-ATV_%282012-h.h.%29.png"/> | AzadTV.az |
| 0 | AzStarTV | [>](http://live.azstartv.com/azstar/smil:azstar.smil/playlist.m3u8) | <img height="20" src="https://i.imgur.com/di3XX5L.png"/> | AzStarTV.ca |
| 0 | AZTV Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=AZTV) | <img height="20" src="https://i.imgur.com/snBMMeH.png"/> | AZTV.az |
| 0 | Baku TV | [>](https://rtmp.baku.tv/live/bakutv_720p.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Baku_TV_%282018%29.png/640px-Baku_TV_%282018%29.png"/> | BakuTV.az |
| 0 | CBC | [>](https://stream.cbctv.az:5443/LiveApp/streams/cbctv.m3u8) | <img height="20" src="https://i.imgur.com/wVT0dwO.png"/> | CBC.az |
| 0 | CBC Sport Ⓖ | [>](https://mn-nl.mncdn.com/cbcsports_live/cbcsports/playlist.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/az/0/04/CBC_Sport_TV_loqo.png"/> | CBCSport.az |
| 0 | Dünya TV Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=Dunya) | <img height="20" src="https://upload.wikimedia.org/wikipedia/az/5/5d/D%C3%BCnya_TV_%282019-h.h.%29.png"/> | DunyaTV.az |
| 0 | İctimai TV Ⓢ | [>](http://109.205.166.68/server124/ictimai_tv/index.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/%C4%B0ctimai_TV_%282021-h.h.%29.svg/470px-%C4%B0ctimai_TV_%282021-h.h.%29.svg.png"/> | IctimaiTV.az |
| 0 | İdman TV Ⓢ | [>](http://109.205.166.68/server124/idman_az/index.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/az/thumb/8/88/%C4%B0dman_Az%C9%99rbaycan_TV_loqo_%282019-h.h.%29.png/640px-%C4%B0dman_Az%C9%99rbaycan_TV_loqo_%282019-h.h.%29.png"/> | IdmanTV.az |
| 0 | AzTV HD Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=AZTV) | <img height="20" src="https://i.imgur.com/snBMMeH.png"/> | AZTV.az |
| 0 | Baku TV HD| [>](https://rtmp.baku.tv/live/bakutv_720p.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Baku_TV_%282018%29.png/640px-Baku_TV_%282018%29.png"/> | BakuTV.az |
| 0 | CBC Azerbaijan TVHD| [>](https://stream.cbctv.az:5443/LiveApp/streams/cbctv.m3u8) | <img height="20" src="https://i.imgur.com/wVT0dwO.png"/> | CBC.az |
| 0 | CBC Sport TV HD Ⓖ | [>](https://mn-nl.mncdn.com/cbcsports_live/cbcsports/playlist.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/az/0/04/CBC_Sport_TV_loqo.png"/> | CBCSport.az |
| 0 | DUNYA TV HD TV Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=Dunya) | <img height="20" src="https://upload.wikimedia.org/wikipedia/az/5/5d/D%C3%BCnya_TV_%282019-h.h.%29.png"/> | DunyaTV.az |
| 0 | İctimai TV HD Ⓢ | [>](http://109.205.166.68/server124/ictimai_tv/index.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/%C4%B0ctimai_TV_%282021-h.h.%29.svg/470px-%C4%B0ctimai_TV_%282021-h.h.%29.svg.png"/> | IctimaiTV.az |
| 0 | İdman TV HD Ⓢ | [>](http://109.205.166.68/server124/idman_az/index.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/az/thumb/8/88/%C4%B0dman_Az%C9%99rbaycan_TV_loqo_%282019-h.h.%29.png/640px-%C4%B0dman_Az%C9%99rbaycan_TV_loqo_%282019-h.h.%29.png"/> | IdmanTV.az |
| 0 | Kanal S | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=KanalS) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Kanal_S_%282022%29.png/616px-Kanal_S_%282022%29.png"/> | KanalS.az |
| 0 | Mədəniyyət TV Ⓢ | [>](https://str.yodacdn.net/medeniyyet/index.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/f/fc/M%C9%99d%C9%99niyy%C9%99t_TV_logo.png"/> | MedeniyyetTV.az |
| 0 | Mədəniyyət TV HD Ⓢ | [>](https://str.yodacdn.net/medeniyyet/index.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/f/fc/M%C9%99d%C9%99niyy%C9%99t_TV_logo.png"/> | MedeniyyetTV.az |
| 0 | Muz TV | [x]() | <img height="20" src="https://i.imgur.com/CjySP1V.png"/> | MuzTV.az |
| 0 | Real TV | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=RealTV) | <img height="20" src="https://i.imgur.com/e2KFL0R.png"/> | RealTV.az |
| 0 | Space TV Ⓢ | [>](http://109.205.166.68/server124/space_tv/index.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Space_TV_loqosu_%282023-h.h.%29.png/296px-Space_TV_loqosu_%282023-h.h.%29.png"/> | SpaceTV.az |
| 0 | Real TV HD | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=RealTV) | <img height="20" src="https://i.imgur.com/e2KFL0R.png"/> | RealTV.az |
| 0 | Space TV HD Ⓢ | [>](http://109.205.166.68/server124/space_tv/index.m3u8) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Space_TV_loqosu_%282023-h.h.%29.png/296px-Space_TV_loqosu_%282023-h.h.%29.png"/> | SpaceTV.az |
| 0 | TMB Azərbaycan | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=TMBAzerbaijan) | <img height="20" src="https://upload.wikimedia.org/wikipedia/az/c/c2/TMB_TV_loqosu.png"/> |
| 0 | Topaz 2 | [x]() | <img height="20" src="https://www.lyngsat.com/logo/tv/tt/topaz_tv_az.png"/> |
| 0 | Xəzər Xəbər | [x]() | <img height="20" src="https://i.imgur.com/AuB8bnq.png"/> | XezerXeber.az |
@ -35,8 +35,8 @@
| # | Channel | Link | Logo | EPG id |
|:---:|:--------------:|:-----:|:----:|:------:|
| 0 | Xəzər TV Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=Xezer) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/a/a5/X%C9%99z%C9%99r_TV_%282023%29.png"/> | XezerTV.az |
| 0 | Xazar TV HD Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=Xezer) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/a/a5/X%C9%99z%C9%99r_TV_%282023%29.png"/> | XezerTV.az |
| 0 | Səhiyyə TV | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=SehiyyeTV) | <img height="20" src="https://upload.wikimedia.org/wikipedia/az/thumb/c/cd/S%C9%99hiyy%C9%99_TV.png/640px-S%C9%99hiyy%C9%99_TV.png"/> | SehiyyeTV.az |
| 0  | MCJ TV SHOP | [x](https://www.tvkaista.net/stream-forwarder/get.php?x=MCJTVShop) | <img height="20" src="https://tvtolive.com/wp-content/uploads/MCJ-TV-Shop-tvtolive.com_.jpg"/> |
| 0 | VIP HD | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=AZ_VIP) | <img height="20" src="https://tvtolive.com/wp-content/uploads/VIP-TV-tvtolive.com_.jpg"/> |
| 0 | MTV Azerbaijan Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=MTVAzerbaijan) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/MTV_Az%C9%99rbaycan_%282022%29.png/622px-MTV_Az%C9%99rbaycan_%282022%29.png"/> | MTVAzerbaijan.az |
| 0 | MTV Azerbaijan HD Ⓢ | [>](https://www.tvkaista.net/stream-forwarder/get.php?x=MTVAzerbaijan) | <img height="20" src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/MTV_Az%C9%99rbaycan_%282022%29.png/622px-MTV_Az%C9%99rbaycan_%282022%29.png"/> | MTVAzerbaijan.az |

View File

@ -3,11 +3,98 @@
import os
import re
EPG_LIST = open('epglist.txt',"r") # for a clean code
COUNTRY_CODES = {
"albania": "AL",
"andorra": "AD",
"argentina": "AR",
"armenia": "AM",
"australia": "AU",
"austria": "AT",
"azerbaijan": "AZ",
"belarus": "BY",
"belgium": "BE",
"bosnia_and_herzegovina": "BA",
"brazil": "BR",
"bulgaria": "BG",
"canada": "CA",
"chad": "TD",
"chile": "CL",
"china": "CN",
"costa_rica": "CR",
"croatia": "HR",
"cyprus": "CY",
"czech_republic": "CZ",
"denmark": "DK",
"dominican_republic": "DO",
"egypt": "EG",
"estonia": "EE",
"faroe_islands": "FO",
"finland": "FI",
"france": "FR",
"georgia": "GE",
"germany": "DE",
"greece": "GR",
"greenland": "GL",
"hong_kong": "HK",
"hongkong": "HK",
"hungary": "HU",
"iceland": "IS",
"india": "IN",
"indonesia": "ID",
"iran": "IR",
"iraq": "IQ",
"ireland": "IE",
"israel": "IL",
"italy": "IT",
"japan": "JP",
"korea": "KR",
"kosovo": "XK",
"latvia": "LV",
"lithuania": "LT",
"luxembourg": "LU",
"macau": "MO",
"malta": "MT",
"mexico": "MX",
"moldova": "MD",
"monaco": "MC",
"montenegro": "ME",
"netherlands": "NL",
"north_korea": "KP",
"north_macedonia": "MK",
"norway": "NO",
"paraguay": "PY",
"peru": "PE",
"poland": "PL",
"portugal": "PT",
"qatar": "QA",
"romania": "RO",
"russia": "RU",
"san_marino": "SM",
"saudi_arabia": "SA",
"serbia": "RS",
"slovakia": "SK",
"slovenia": "SI",
"somalia": "SO",
"spain": "ES",
"spain_vod": "ES",
"sweden": "SE",
"switzerland": "CH",
"taiwan": "TW",
"trinidad": "TT",
"turkey": "TR",
"uk": "GB",
"ukraine": "UA",
"united_arab_emirates": "AE",
"usa": "US",
"usa_vod": "US",
"venezuela": "VE",
}
class Channel:
def __init__(self, group, md_line):
def __init__(self, group, md_line, country_code=""):
self.group = group
self.country_code = country_code
md_line = md_line.strip()
parts = md_line.split("|")
self.number = parts[1].strip()
@ -22,39 +109,49 @@ class Channel:
self.epg = None
def to_m3u_line(self):
country = f' tvg-country="{self.country_code}"' if self.country_code else ""
if self.epg is None:
return (f'#EXTINF:-1 tvg-name="{self.name}" tvg-logo="{self.logo}" group-title="{self.group}",{self.name}\n{self.url}')
return (f'#EXTINF:-1 tvg-name="{self.name}" tvg-logo="{self.logo}"{country} group-title="{self.group}",{self.name}\n{self.url}')
else:
return (f'#EXTINF:-1 tvg-name="{self.name}" tvg-logo="{self.logo}" tvg-id="{self.epg}" group-title="{self.group}",{self.name}\n{self.url}')
return (f'#EXTINF:-1 tvg-name="{self.name}" tvg-logo="{self.logo}" tvg-id="{self.epg}"{country} group-title="{self.group}",{self.name}\n{self.url}')
def main():
dir_playlists = 'playlists'
if not (os.path.isdir(dir_playlists)):
base_dir = os.path.dirname(os.path.abspath(__file__))
lists_dir = os.path.join(base_dir, "lists")
dir_playlists = os.path.join(base_dir, "playlists")
if not os.path.isdir(dir_playlists):
os.mkdir(dir_playlists)
with open("playlist.m3u8", "w", encoding='utf-8') as playlist:
processed_epg_list = ", ".join(EPG_LIST).replace('\n', '')
head_playlist = f'#EXTM3U x-tvg-url="{processed_epg_list}"'
print(f'#EXTM3U x-tvg-url="{processed_epg_list}"', file=playlist)
os.chdir("lists")
for filename in sorted(os.listdir(".")):
with open(os.path.join(base_dir, "epglist.txt"), encoding='utf-8') as epg_file:
epg_urls = [line.strip() for line in epg_file if line.strip()]
processed_epg_list = ", ".join(epg_urls)
head_playlist = f'#EXTM3U x-tvg-url="{processed_epg_list}"\n'
with open(os.path.join(base_dir, "playlist.m3u8"), "w", encoding='utf-8') as playlist:
playlist.write(head_playlist)
for filename in sorted(os.listdir(lists_dir)):
if filename == "README.md" or not filename.endswith(".md"):
continue
with open(filename, encoding='utf-8') as markup_file:
file_country = os.path.join("..", dir_playlists, "playlist_" + filename[:-3:] + ".m3u8")
playlist_country = open(file_country, "w", encoding='utf-8')
playlist_country.write(head_playlist + "\n")
group = filename.replace(".md", "").title()
print(f"Generating {group}")
markup_path = os.path.join(lists_dir, filename)
country_path = os.path.join(dir_playlists, "playlist_" + filename[:-3] + ".m3u8")
country_key = filename[:-3]
group = country_key.replace("_", " ").title()
country_code = COUNTRY_CODES.get(country_key, "")
print(f"Generating {group}")
with open(markup_path, encoding='utf-8') as markup_file, \
open(country_path, "w", encoding='utf-8') as playlist_country:
playlist_country.write(head_playlist)
for line in markup_file:
if "<h1>" in line.lower() and "</h1>" in line.lower():
group = re.sub('<[^<>]+>', '', line.strip())
if not "[>]" in line:
if "[>]" not in line:
continue
channel = Channel(group, line)
print(channel.to_m3u_line(), file=playlist)
print(channel.to_m3u_line(), file=playlist_country)
playlist_country.close()
channel = Channel(group, line, country_code)
m3u_line = channel.to_m3u_line()
print(m3u_line, file=playlist)
print(m3u_line, file=playlist_country)
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long