From 87f60bcc7c837c494c589a266d2fa5040ff31d59 Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Fri, 22 Aug 2025 19:45:12 +0300
Subject: [PATCH 01/15] Update dependencies
---
package-lock.json | 91 ++++++++++++++++++++---------------------------
package.json | 1 +
2 files changed, 40 insertions(+), 52 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index a96f29a725..76b8c282a2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+ "@alex_neo/jest-expect-message": "^1.0.5",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.32.0",
"@freearhey/core": "^0.10.2",
@@ -45,6 +46,11 @@
"tsx": "^4.20.3"
}
},
+ "node_modules/@alex_neo/jest-expect-message": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@alex_neo/jest-expect-message/-/jest-expect-message-1.0.5.tgz",
+ "integrity": "sha512-1eBykZCd0pPGl5qKtV6Z5ARA6yuhXzHsVN2h5GH5/H6svYa37Jr7vMio5OFpiw1LBHtscrZs7amSkZkcwm0cvQ=="
+ },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -1254,14 +1260,13 @@
}
},
"node_modules/@inquirer/editor": {
- "version": "4.2.15",
- "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz",
- "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==",
- "license": "MIT",
+ "version": "4.2.17",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.17.tgz",
+ "integrity": "sha512-r6bQLsyPSzbWrZZ9ufoWL+CztkSatnJ6uSxqd6N+o41EZC51sQeWOzI6s5jLb+xxTWxl7PlUppqm8/sow241gg==",
"dependencies": {
"@inquirer/core": "^10.1.15",
- "@inquirer/type": "^3.0.8",
- "external-editor": "^3.1.0"
+ "@inquirer/external-editor": "^1.0.1",
+ "@inquirer/type": "^3.0.8"
},
"engines": {
"node": ">=18"
@@ -1297,6 +1302,26 @@
}
}
},
+ "node_modules/@inquirer/external-editor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz",
+ "integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==",
+ "dependencies": {
+ "chardet": "^2.1.0",
+ "iconv-lite": "^0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@inquirer/figures": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz",
@@ -3853,10 +3878,9 @@
}
},
"node_modules/chardet": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
- "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
- "license": "MIT"
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz",
+ "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA=="
},
"node_modules/ci-info": {
"version": "4.3.0",
@@ -4547,20 +4571,6 @@
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/external-editor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
- "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
- "license": "MIT",
- "dependencies": {
- "chardet": "^0.7.0",
- "iconv-lite": "^0.4.24",
- "tmp": "^0.0.33"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
@@ -5033,12 +5043,11 @@
}
},
"node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "license": "MIT",
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
@@ -6848,15 +6857,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/os-tmpdir": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
- "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/outvariant": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
@@ -7280,8 +7280,7 @@
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "license": "MIT"
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "7.7.2",
@@ -7667,18 +7666,6 @@
"resolved": "https://registry.npmjs.org/timer-node/-/timer-node-5.0.9.tgz",
"integrity": "sha512-zXxCE/5/YDi0hY9pygqgRqjRbrFRzigYxOudG0I3syaqAAmX9/w9sxex1bNFCN6c1S66RwPtEIJv65dN+1psew=="
},
- "node_modules/tmp": {
- "version": "0.0.33",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
- "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
- "license": "MIT",
- "dependencies": {
- "os-tmpdir": "~1.0.2"
- },
- "engines": {
- "node": ">=0.6.0"
- }
- },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
diff --git a/package.json b/package.json
index f9fafdaee9..4c68d9e665 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"private": true,
"license": "MIT",
"dependencies": {
+ "@alex_neo/jest-expect-message": "^1.0.5",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.32.0",
"@freearhey/core": "^0.10.2",
From c09fd73503b9e45a207fcb159aa1bb2f9788495f Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sat, 23 Aug 2025 17:46:46 +0300
Subject: [PATCH 02/15] Update tests/__data__
---
.../.gh-pages/countries/ad.m3u | 3 -
.../.gh-pages/countries/ca.m3u | 2 -
.../.gh-pages/countries/undefined.m3u | 15 -
.../.gh-pages/index.country.m3u | 22 --
.../.gh-pages/index.region.m3u | 28 --
.../.gh-pages/regions/amer.m3u | 2 -
.../.gh-pages/regions/emea.m3u | 2 -
.../.gh-pages/regions/eur.m3u | 2 -
.../.gh-pages/regions/int.m3u | 5 -
.../.gh-pages/regions/nam.m3u | 2 -
.../.gh-pages/regions/noram.m3u | 2 -
.../.gh-pages/regions/undefined.m3u | 15 -
.../.gh-pages/subdivisions/ca-on.m3u | 2 -
.../playlist_generate/logs/generators.log | 271 +++++++-----------
.../expected/readme_update/playlists.md | 1 -
tests/__data__/input/data/cities.json | 1 +
tests/__data__/input/data/feeds.json | 6 +-
tests/__data__/input/data/regions.json | 2 +-
.../input/readme_update/generators.log | 1 -
19 files changed, 111 insertions(+), 273 deletions(-)
delete mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/countries/ad.m3u
delete mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u
delete mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/regions/int.m3u
delete mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/regions/undefined.m3u
create mode 100644 tests/__data__/input/data/cities.json
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/countries/ad.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/countries/ad.m3u
deleted file mode 100644
index 92a59e8a10..0000000000
--- a/tests/__data__/expected/playlist_generate/.gh-pages/countries/ad.m3u
+++ /dev/null
@@ -1,3 +0,0 @@
-#EXTM3U
-#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Undefined",ATV
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/countries/ca.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/countries/ca.m3u
index 8aeaaae90e..754a6969c0 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/countries/ca.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/countries/ca.m3u
@@ -1,5 +1,3 @@
#EXTM3U
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
-http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u
deleted file mode 100644
index 1770572503..0000000000
--- a/tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u
+++ /dev/null
@@ -1,15 +0,0 @@
-#EXTM3U
-#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
-#EXTVLCOPT:http-referrer=http://imn.iq
-#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
-#KODIPROP:inputstream=inputstream.adaptive
-#KODIPROP:inputstream.adaptive.manifest_type=mpd
-#KODIPROP:inputstream.adaptive.license_type=com.widevine.alpha
-#KODIPROP:inputstream.adaptive.license_key=https://drm.ors.at/acquire-license/widevine?BrandGuid=13f2e056-53fe-4469-ba6d-999970dbe549&userToken=v9ZVSksv4S7rT55o10dmYNRa4asye3z05eWCFxD%2FFYIlTJEpuf6tF8asPcyQOFq0h5opS%2B6WoMxnshWkihpHq5qrdrBEZ69piE94J9Feh385snGOqK3PYO7tLLjxmsCAe%2B9%2BNnurSSO5RCAIRsL125nSj1eOR%2F1GSKOgGH80HK2FDLiePxPkeaAxuWzacNBB%2FqnIGGxfe3GlmN65cU9F8WEpKFDlaxW%2Fv3ZSLAp3%2BZEq1aZXJ6Oz%2Fi0diD0EybH7|Content-Type=application/octet-stream|R{SSM}|
-http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8|Referer="https://referer.xyz/"|User-Agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1"|Origin="https://origin.xyz"
-#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="https://i.imgur.com/CnhTn8i.png" group-title="Undefined",ATV HD
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
-#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
-http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
-#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="https://i.imgur.com/ciTJrnl.png" group-title="Undefined",Zoo (720p)
-https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
index 1848f28016..063a191f7e 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
@@ -1,10 +1,6 @@
#EXTM3U
-#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Andorra",ATV
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Canada",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Canada",Meteomedia
-http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Kazakhstan",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Kyrgyzstan",ЭлТР (480p) [Not 24/7]
@@ -17,21 +13,3 @@ http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Uzbekistan",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="International",BBC News HD
-http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
-#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="International",Duna World (576i)
-http://146.59.85.40:89/dunaworld/index.m3u8
-#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
-#EXTVLCOPT:http-referrer=http://imn.iq
-#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
-#KODIPROP:inputstream=inputstream.adaptive
-#KODIPROP:inputstream.adaptive.manifest_type=mpd
-#KODIPROP:inputstream.adaptive.license_type=com.widevine.alpha
-#KODIPROP:inputstream.adaptive.license_key=https://drm.ors.at/acquire-license/widevine?BrandGuid=13f2e056-53fe-4469-ba6d-999970dbe549&userToken=v9ZVSksv4S7rT55o10dmYNRa4asye3z05eWCFxD%2FFYIlTJEpuf6tF8asPcyQOFq0h5opS%2B6WoMxnshWkihpHq5qrdrBEZ69piE94J9Feh385snGOqK3PYO7tLLjxmsCAe%2B9%2BNnurSSO5RCAIRsL125nSj1eOR%2F1GSKOgGH80HK2FDLiePxPkeaAxuWzacNBB%2FqnIGGxfe3GlmN65cU9F8WEpKFDlaxW%2Fv3ZSLAp3%2BZEq1aZXJ6Oz%2Fi0diD0EybH7|Content-Type=application/octet-stream|R{SSM}|
-http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8|Referer="https://referer.xyz/"|User-Agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1"|Origin="https://origin.xyz"
-#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="https://i.imgur.com/CnhTn8i.png" group-title="Undefined",ATV HD
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
-#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
-http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
-#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="https://i.imgur.com/ciTJrnl.png" group-title="Undefined",Zoo (720p)
-https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u
index 3fd36e2146..0488050b70 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u
@@ -1,8 +1,6 @@
#EXTM3U
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Americas",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Americas",Meteomedia
-http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Asia",ЛДПР ТВ (1080p)
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Asia",ЭлТР (480p) [Not 24/7]
@@ -13,41 +11,15 @@ http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Commonwealth of Independent States",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Europe",ATV
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe",ЛДПР ТВ (1080p)
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Europe, the Middle East and Africa",ATV
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe, the Middle East and Africa",ЛДПР ТВ (1080p)
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe, the Middle East and Africa",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="North America",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="North America",Meteomedia
-http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Northern America",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Northern America",Meteomedia
-http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
-#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="International",BBC News HD
-http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
-#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="International",Duna World (576i)
-http://146.59.85.40:89/dunaworld/index.m3u8
-#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
-#EXTVLCOPT:http-referrer=http://imn.iq
-#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
-#KODIPROP:inputstream=inputstream.adaptive
-#KODIPROP:inputstream.adaptive.manifest_type=mpd
-#KODIPROP:inputstream.adaptive.license_type=com.widevine.alpha
-#KODIPROP:inputstream.adaptive.license_key=https://drm.ors.at/acquire-license/widevine?BrandGuid=13f2e056-53fe-4469-ba6d-999970dbe549&userToken=v9ZVSksv4S7rT55o10dmYNRa4asye3z05eWCFxD%2FFYIlTJEpuf6tF8asPcyQOFq0h5opS%2B6WoMxnshWkihpHq5qrdrBEZ69piE94J9Feh385snGOqK3PYO7tLLjxmsCAe%2B9%2BNnurSSO5RCAIRsL125nSj1eOR%2F1GSKOgGH80HK2FDLiePxPkeaAxuWzacNBB%2FqnIGGxfe3GlmN65cU9F8WEpKFDlaxW%2Fv3ZSLAp3%2BZEq1aZXJ6Oz%2Fi0diD0EybH7|Content-Type=application/octet-stream|R{SSM}|
-http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8|Referer="https://referer.xyz/"|User-Agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1"|Origin="https://origin.xyz"
-#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="https://i.imgur.com/CnhTn8i.png" group-title="Undefined",ATV HD
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
-#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
-http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
-#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="https://i.imgur.com/ciTJrnl.png" group-title="Undefined",Zoo (720p)
-https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u
index 8aeaaae90e..754a6969c0 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u
@@ -1,5 +1,3 @@
#EXTM3U
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
-http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u
index 87d85279d7..6f6d448e87 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u
@@ -1,6 +1,4 @@
#EXTM3U
-#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Undefined",ATV
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u
index 87d85279d7..6f6d448e87 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u
@@ -1,6 +1,4 @@
#EXTM3U
-#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Undefined",ATV
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/int.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/int.m3u
deleted file mode 100644
index c549c09ce1..0000000000
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/int.m3u
+++ /dev/null
@@ -1,5 +0,0 @@
-#EXTM3U
-#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
-http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
-#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
-http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u
index 8aeaaae90e..754a6969c0 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u
@@ -1,5 +1,3 @@
#EXTM3U
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
-http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u
index 8aeaaae90e..754a6969c0 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u
@@ -1,5 +1,3 @@
#EXTM3U
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
-http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/undefined.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/undefined.m3u
deleted file mode 100644
index 1770572503..0000000000
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/undefined.m3u
+++ /dev/null
@@ -1,15 +0,0 @@
-#EXTM3U
-#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
-#EXTVLCOPT:http-referrer=http://imn.iq
-#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
-#KODIPROP:inputstream=inputstream.adaptive
-#KODIPROP:inputstream.adaptive.manifest_type=mpd
-#KODIPROP:inputstream.adaptive.license_type=com.widevine.alpha
-#KODIPROP:inputstream.adaptive.license_key=https://drm.ors.at/acquire-license/widevine?BrandGuid=13f2e056-53fe-4469-ba6d-999970dbe549&userToken=v9ZVSksv4S7rT55o10dmYNRa4asye3z05eWCFxD%2FFYIlTJEpuf6tF8asPcyQOFq0h5opS%2B6WoMxnshWkihpHq5qrdrBEZ69piE94J9Feh385snGOqK3PYO7tLLjxmsCAe%2B9%2BNnurSSO5RCAIRsL125nSj1eOR%2F1GSKOgGH80HK2FDLiePxPkeaAxuWzacNBB%2FqnIGGxfe3GlmN65cU9F8WEpKFDlaxW%2Fv3ZSLAp3%2BZEq1aZXJ6Oz%2Fi0diD0EybH7|Content-Type=application/octet-stream|R{SSM}|
-http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8|Referer="https://referer.xyz/"|User-Agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1"|Origin="https://origin.xyz"
-#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="https://i.imgur.com/CnhTn8i.png" group-title="Undefined",ATV HD
-https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
-#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
-http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
-#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="https://i.imgur.com/ciTJrnl.png" group-title="Undefined",Zoo (720p)
-https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u
index 8aeaaae90e..754a6969c0 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u
@@ -1,5 +1,3 @@
#EXTM3U
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
-http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/logs/generators.log b/tests/__data__/expected/playlist_generate/logs/generators.log
index 2177b05c0e..903c4022f8 100644
--- a/tests/__data__/expected/playlist_generate/logs/generators.log
+++ b/tests/__data__/expected/playlist_generate/logs/generators.log
@@ -5,226 +5,167 @@
{"type":"raw","filepath":"raw/uk.m3u","count":1}
{"type":"raw","filepath":"raw/unsorted.m3u","count":4}
{"type":"category","filepath":"categories/auto.m3u","count":0}
-{"type":"category","filepath":"categories/comedy.m3u","count":0}
-{"type":"category","filepath":"categories/business.m3u","count":0}
-{"type":"category","filepath":"categories/cooking.m3u","count":0}
{"type":"category","filepath":"categories/animation.m3u","count":0}
+{"type":"category","filepath":"categories/business.m3u","count":0}
{"type":"category","filepath":"categories/classic.m3u","count":0}
+{"type":"category","filepath":"categories/cooking.m3u","count":0}
+{"type":"category","filepath":"categories/comedy.m3u","count":0}
+{"type":"category","filepath":"categories/education.m3u","count":0}
{"type":"category","filepath":"categories/documentary.m3u","count":0}
{"type":"category","filepath":"categories/family.m3u","count":0}
{"type":"category","filepath":"categories/culture.m3u","count":0}
-{"type":"category","filepath":"categories/education.m3u","count":0}
-{"type":"category","filepath":"categories/general.m3u","count":3}
-{"type":"category","filepath":"categories/kids.m3u","count":0}
-{"type":"category","filepath":"categories/lifestyle.m3u","count":0}
-{"type":"category","filepath":"categories/movies.m3u","count":0}
-{"type":"category","filepath":"categories/music.m3u","count":0}
-{"type":"category","filepath":"categories/outdoor.m3u","count":0}
-{"type":"category","filepath":"categories/science.m3u","count":0}
-{"type":"category","filepath":"categories/news.m3u","count":1}
-{"type":"category","filepath":"categories/religious.m3u","count":0}
-{"type":"category","filepath":"categories/series.m3u","count":0}
-{"type":"category","filepath":"categories/relax.m3u","count":0}
-{"type":"category","filepath":"categories/sports.m3u","count":0}
-{"type":"category","filepath":"categories/undefined.m3u","count":7}
-{"type":"category","filepath":"categories/shop.m3u","count":0}
{"type":"category","filepath":"categories/entertainment.m3u","count":0}
+{"type":"category","filepath":"categories/general.m3u","count":3}
+{"type":"category","filepath":"categories/lifestyle.m3u","count":0}
+{"type":"category","filepath":"categories/legislative.m3u","count":0}
+{"type":"category","filepath":"categories/movies.m3u","count":0}
+{"type":"category","filepath":"categories/news.m3u","count":1}
+{"type":"category","filepath":"categories/kids.m3u","count":0}
+{"type":"category","filepath":"categories/relax.m3u","count":0}
+{"type":"category","filepath":"categories/music.m3u","count":0}
+{"type":"category","filepath":"categories/science.m3u","count":0}
+{"type":"category","filepath":"categories/outdoor.m3u","count":0}
+{"type":"category","filepath":"categories/series.m3u","count":0}
+{"type":"category","filepath":"categories/shop.m3u","count":0}
{"type":"category","filepath":"categories/travel.m3u","count":0}
{"type":"category","filepath":"categories/xxx.m3u","count":1}
-{"type":"category","filepath":"categories/legislative.m3u","count":0}
{"type":"category","filepath":"categories/weather.m3u","count":1}
+{"type":"category","filepath":"categories/undefined.m3u","count":7}
+{"type":"category","filepath":"categories/sports.m3u","count":0}
+{"type":"category","filepath":"categories/religious.m3u","count":0}
{"type":"language","filepath":"languages/cat.m3u","count":1}
{"type":"language","filepath":"languages/rus.m3u","count":1}
-{"type":"language","filepath":"languages/eng.m3u","count":1}
{"type":"language","filepath":"languages/undefined.m3u","count":8}
-{"type":"country","filepath":"countries/ca.m3u","count":2}
-{"type":"country","filepath":"countries/ad.m3u","count":1}
-{"type":"country","filepath":"countries/ru.m3u","count":1}
-{"type":"country","filepath":"countries/uz.m3u","count":1}
+{"type":"language","filepath":"languages/eng.m3u","count":1}
+{"type":"country","filepath":"countries/ca.m3u","count":1}
{"type":"country","filepath":"countries/kz.m3u","count":1}
+{"type":"country","filepath":"countries/ru.m3u","count":1}
{"type":"country","filepath":"countries/tj.m3u","count":1}
{"type":"country","filepath":"countries/tm.m3u","count":1}
-{"type":"country","filepath":"countries/undefined.m3u","count":4}
{"type":"country","filepath":"countries/kg.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ad-07.m3u","count":1}
-{"type":"region","filepath":"regions/afr.m3u","count":0}
-{"type":"subdivision","filepath":"subdivisions/ad-02.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ad-04.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ad-08.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ad-03.m3u","count":1}
+{"type":"country","filepath":"countries/uz.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-ab.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ad-05.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-bc.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-nl.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ad-06.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-mb.m3u","count":1}
+{"type":"region","filepath":"regions/afr.m3u","count":0}
{"type":"subdivision","filepath":"subdivisions/ca-nb.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-mb.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-bc.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-nt.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-nu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":2}
-{"type":"subdivision","filepath":"subdivisions/ca-ns.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-pe.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-nl.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-qc.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-sk.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kg-j.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kg-b.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kg-t.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-yt.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kg-c.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kg-gb.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kg-n.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kg-y.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-ala.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kg-go.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-alm.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-akm.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-zap.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-aty.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-man.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-yuz.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-kus.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-akt.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-kar.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-kzy.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-ast.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-shy.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-pav.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-vos.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-ns.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-nu.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-ad.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-sev.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-al.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-yt.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-pe.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-sk.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-ba.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-da.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-al.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-kr.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-bu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-in.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-kk.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-kl.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/kz-zha.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-me.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-se.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-mo.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-da.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-ko.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ty.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kr.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-me.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-in.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-alt.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-amu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-bel.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ast.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-sa.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-che.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-cu.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-se.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-ty.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-ta.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-mo.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-ark.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-bel.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-sa.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-amu.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-ast.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-ce.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-bry.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-che.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-cu.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-irk.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-chu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kb.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-iva.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-klu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kc.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ark.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kda.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kha.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-kgd.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kya.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-khm.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kgn.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kem.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kir.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-mag.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-kb.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-klu.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-kam.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-krs.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-len.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-kc.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-khm.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-kha.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-kem.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-iva.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-kda.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-kya.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-kir.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-kgn.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-mos.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-kos.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-lip.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-krs.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-mag.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-len.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-mur.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-lip.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-ngr.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-nen.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-mow.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-niz.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-nen.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ngr.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ore.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-oms.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-orl.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-nvs.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-mos.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-psk.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ros.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-per.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-pnz.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-nvs.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-oms.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-per.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-ore.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-pri.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-sam.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-ros.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-sak.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-rya.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-spe.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-sar.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-tam.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-tom.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-smo.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-sta.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-tul.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-sam.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-rya.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-smo.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-psk.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-spe.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-sve.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-vla.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-tve.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-tam.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-ud.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-vgg.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-vor.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-uly.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-zab.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-vlg.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-tom.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-sar.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ru-tyu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tj-du.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-yan.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tj-kt.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tj-gb.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tj-ra.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tm-a.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-yev.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-yar.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tj-su.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tm-b.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tm-l.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tm-m.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/tm-d.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-ji.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-nw.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-bu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-qr.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-an.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-fa.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-qa.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-si.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-sa.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-tk.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-ng.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-xo.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/uz-su.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-tve.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-uly.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-vor.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-vgg.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-tul.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-vlg.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-vla.m3u","count":1}
{"type":"region","filepath":"regions/apac.m3u","count":0}
-{"type":"region","filepath":"regions/amer.m3u","count":2}
-{"type":"region","filepath":"regions/asean.m3u","count":0}
+{"type":"subdivision","filepath":"subdivisions/ru-yan.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-yar.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ru-yev.m3u","count":1}
{"type":"region","filepath":"regions/arab.m3u","count":0}
-{"type":"region","filepath":"regions/carib.m3u","count":0}
+{"type":"region","filepath":"regions/asean.m3u","count":0}
+{"type":"subdivision","filepath":"subdivisions/ru-zab.m3u","count":1}
+{"type":"region","filepath":"regions/amer.m3u","count":1}
+{"type":"region","filepath":"regions/asia.m3u","count":2}
{"type":"region","filepath":"regions/cis.m3u","count":2}
{"type":"region","filepath":"regions/cas.m3u","count":1}
-{"type":"region","filepath":"regions/cenamer.m3u","count":0}
+{"type":"region","filepath":"regions/carib.m3u","count":0}
+{"type":"region","filepath":"regions/emea.m3u","count":2}
{"type":"region","filepath":"regions/lac.m3u","count":0}
-{"type":"region","filepath":"regions/emea.m3u","count":3}
-{"type":"region","filepath":"regions/eur.m3u","count":3}
-{"type":"region","filepath":"regions/mena.m3u","count":0}
{"type":"region","filepath":"regions/hispam.m3u","count":0}
+{"type":"region","filepath":"regions/cenamer.m3u","count":0}
{"type":"region","filepath":"regions/latam.m3u","count":0}
+{"type":"region","filepath":"regions/nam.m3u","count":1}
{"type":"region","filepath":"regions/maghreb.m3u","count":0}
-{"type":"region","filepath":"regions/asia.m3u","count":2}
-{"type":"region","filepath":"regions/oce.m3u","count":0}
-{"type":"region","filepath":"regions/noram.m3u","count":2}
-{"type":"region","filepath":"regions/nord.m3u","count":0}
-{"type":"region","filepath":"regions/nam.m3u","count":2}
-{"type":"region","filepath":"regions/int.m3u","count":2}
-{"type":"region","filepath":"regions/southam.m3u","count":0}
+{"type":"region","filepath":"regions/eur.m3u","count":2}
+{"type":"region","filepath":"regions/mena.m3u","count":0}
{"type":"region","filepath":"regions/mideast.m3u","count":0}
-{"type":"region","filepath":"regions/wafr.m3u","count":0}
+{"type":"region","filepath":"regions/nord.m3u","count":0}
{"type":"region","filepath":"regions/sas.m3u","count":0}
+{"type":"region","filepath":"regions/noram.m3u","count":1}
+{"type":"region","filepath":"regions/oce.m3u","count":0}
+{"type":"region","filepath":"regions/wafr.m3u","count":0}
{"type":"region","filepath":"regions/ssa.m3u","count":0}
-{"type":"region","filepath":"regions/undefined.m3u","count":4}
+{"type":"region","filepath":"regions/southam.m3u","count":0}
{"type":"source","filepath":"sources/in.m3u","count":1}
{"type":"source","filepath":"sources/unsorted.m3u","count":4}
{"type":"source","filepath":"sources/ca.m3u","count":2}
@@ -233,6 +174,6 @@
{"type":"source","filepath":"sources/kg.m3u","count":1}
{"type":"index","filepath":"index.m3u","count":11}
{"type":"index","filepath":"index.category.m3u","count":12}
-{"type":"index","filepath":"index.country.m3u","count":15}
+{"type":"index","filepath":"index.country.m3u","count":7}
{"type":"index","filepath":"index.language.m3u","count":11}
-{"type":"index","filepath":"index.region.m3u","count":23}
+{"type":"index","filepath":"index.region.m3u","count":12}
diff --git a/tests/__data__/expected/readme_update/playlists.md b/tests/__data__/expected/readme_update/playlists.md
index 74dd5d970f..9736435104 100644
--- a/tests/__data__/expected/readme_update/playlists.md
+++ b/tests/__data__/expected/readme_update/playlists.md
@@ -188,7 +188,6 @@ Same thing, but split up into separate files:
| South Asia | 1 | https://iptv-org.github.io/iptv/regions/sas.m3u |
| Sub-Saharan Africa | 0 | https://iptv-org.github.io/iptv/regions/ssa.m3u |
| West Africa | 0 | https://iptv-org.github.io/iptv/regions/wafr.m3u |
- | Worldwide | 1 | https://iptv-org.github.io/iptv/regions/int.m3u |
| Undefined | 2 | https://iptv-org.github.io/iptv/regions/undefined.m3u |
diff --git a/tests/__data__/input/data/cities.json b/tests/__data__/input/data/cities.json
new file mode 100644
index 0000000000..7bbd99d9b1
--- /dev/null
+++ b/tests/__data__/input/data/cities.json
@@ -0,0 +1 @@
+[{"country":"AD","subdivision":"AD-02","code":"ADCAN","name":"Canillo","wikidata_id":"Q386802"},{"country":"CA","subdivision":"CA-ON","code":"CAAAC","name":"Ailsa Craig","wikidata_id":"Q65963197"}]
\ No newline at end of file
diff --git a/tests/__data__/input/data/feeds.json b/tests/__data__/input/data/feeds.json
index 7472d177a0..5383440571 100644
--- a/tests/__data__/input/data/feeds.json
+++ b/tests/__data__/input/data/feeds.json
@@ -5,7 +5,7 @@
"name": "SD",
"is_main": true,
"broadcast_area": [
- "c/AD"
+ "ct/ADCAN"
],
"languages": [
"cat"
@@ -37,7 +37,7 @@
"name": "SD",
"is_main": true,
"broadcast_area": [
- "r/INT"
+ "r/WW"
],
"languages": [
"eng"
@@ -148,7 +148,7 @@
"name": "SD",
"is_main": true,
"broadcast_area": [
- "s/CA-ON"
+ "ct/CAAAC"
],
"languages": [
"fru"
diff --git a/tests/__data__/input/data/regions.json b/tests/__data__/input/data/regions.json
index 0741930a13..e1f7b1e672 100644
--- a/tests/__data__/input/data/regions.json
+++ b/tests/__data__/input/data/regions.json
@@ -1 +1 @@
-[{"code":"AFR","name":"Africa","countries":["AO","BF","BI","BJ","BW","CD","CF","CG","CI","CM","CV","DJ","DZ","EG","EH","ER","ET","GA","GH","GM","GN","GQ","GW","KE","KM","LR","LS","LY","MA","MG","ML","MR","MU","MW","MZ","NA","NE","NG","RE","RW","SC","SD","SH","SL","SN","SO","SS","ST","SZ","TD","TF","TG","TN","TZ","UG","YT","ZA","ZM","ZW"]},{"code":"AMER","name":"Americas","countries":["AG","AI","AR","AW","BB","BL","BM","BO","BR","BS","BV","BZ","CA","CL","CO","CR","CU","CW","DM","DO","EC","FK","GD","GF","GL","GP","GS","GT","GY","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PE","PM","PR","PY","SR","SV","SX","TC","TT","US","UY","VC","VE","VG","VI"]},{"code":"APAC","name":"Asia-Pacific","countries":["AF","AS","AU","BD","BN","BT","CK","CN","FJ","FM","GU","ID","IN","JP","KH","KI","KP","KR","LA","LK","MH","MM","MN","MP","MV","MY","NC","NF","NP","NR","NU","NZ","PF","PG","PH","PK","PN","PW","SB","SG","TH","TK","TL","TO","TV","TW","VN","VU","WF","WS"]},{"code":"ARAB","name":"Arab world","countries":["AE","BH","DJ","DZ","EG","IQ","JO","KM","KW","LB","LY","MA","MR","OM","PS","QA","SA","SD","SO","SY","TN","YE"]},{"code":"ASEAN","name":"Association of Southeast Asian Nations","countries":["BN","KH","ID","LA","MY","MM","PH","SG","TH","VN"]},{"code":"ASIA","name":"Asia","countries":["AE","AF","AM","AZ","BD","BH","BN","BT","CN","CY","GE","ID","IL","IN","IQ","IR","JO","JP","KG","KH","KP","KR","KW","KZ","LA","LB","LK","MM","MN","MV","MY","NP","OM","PH","PK","PS","QA","RU","SA","SG","SY","TH","TJ","TL","TM","TR","TW","UZ","VN","YE"]},{"code":"CARIB","name":"Caribbean","countries":["AG","AI","AW","BB","BL","BS","CU","CW","DM","DO","GD","GP","HT","JM","KN","KY","LC","MF","MQ","MS","PR","SX","TC","TT","VC","VG","VI"]},{"code":"CAS","name":"Central Asia","countries":["KG","KZ","TJ","TM","UZ"]},{"code":"CENAMER","name":"Central America","countries":["BZ","CR","SV","GT","HN","NI","PA"]},{"code":"CIS","name":"Commonwealth of Independent States","countries":["AM","AZ","BY","KG","KZ","MD","RU","TJ","UZ"]},{"code":"EMEA","name":"Europe, the Middle East and Africa","countries":["AD","AE","AL","AM","AO","AT","AZ","BA","BE","BF","BG","BH","BI","BJ","BW","BY","CD","CF","CG","CH","CI","CM","CV","CY","CZ","DE","DJ","DK","DZ","EE","EG","EH","ER","ES","ET","FI","FR","GA","GE","GH","GM","GN","GQ","GR","GW","HR","HU","IE","IQ","IR","IS","IT","JO","KE","KM","KW","KZ","LB","LI","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MG","MK","ML","MR","MT","MU","MW","MZ","NA","NE","NG","NL","NO","OM","PL","PS","PT","QA","RE","RO","RS","RU","RW","SA","SC","SD","SE","SH","SI","SK","SL","SM","SN","SO","SS","ST","SY","SZ","TD","TF","TG","TN","TR","TZ","UA","UG","UK","VA","YE","YT","ZA","ZM","ZW"]},{"code":"EUR","name":"Europe","countries":["AD","AL","AM","AT","AZ","BA","BE","BG","BY","CH","CY","CZ","DE","DK","EE","ES","FI","FR","GE","GR","HR","HU","IE","IS","IT","KZ","LI","LT","LU","LV","MC","MD","ME","MK","MT","NL","NO","PL","PT","RO","RS","RU","SE","SI","SK","SM","TR","UA","UK","VA"]},{"code":"HISPAM","name":"Hispanic America","countries":["AR","BO","CL","CO","CR","CU","DO","EC","GT","HN","MX","NI","PA","PE","PR","PY","SV","UY","VE"]},{"code":"INT","name":"Worldwide","countries":["AD","AE","AF","AG","AI","AL","AM","AO","AQ","AR","AS","AT","AU","AW","AX","AZ","BA","BB","BD","BE","BF","BG","BH","BI","BJ","BL","BM","BN","BO","BQ","BR","BS","BT","BV","BW","BY","BZ","CA","CC","CD","CF","CG","CH","CI","CK","CL","CM","CN","CO","CR","CU","CV","CW","CX","CY","CZ","DE","DJ","DK","DM","DO","DZ","EC","EE","EG","EH","ER","ES","ET","FI","FJ","FK","FM","FO","FR","GA","UK","GD","GE","GF","GG","GH","GI","GL","GM","GN","GP","GQ","GR","GS","GT","GU","GW","GY","HK","HM","HN","HR","HT","HU","ID","IE","IL","IM","IN","IO","IQ","IR","IS","IT","JE","JM","JO","JP","KE","KG","KH","KI","KM","KN","KP","KR","KW","KY","KZ","LA","LB","LC","LI","LK","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MF","MG","MH","MK","ML","MM","MN","MO","MP","MQ","MR","MS","MT","MU","MV","MW","MX","MY","MZ","NA","NC","NE","NF","NG","NI","NL","NO","NP","NR","NU","NZ","OM","PA","PE","PF","PG","PH","PK","PL","PM","PN","PR","PS","PT","PW","PY","QA","RE","RO","RS","RU","RW","SA","SB","SC","SD","SE","SG","SH","SI","SJ","SK","SL","SM","SN","SO","SR","SS","ST","SV","SX","SY","SZ","TC","TD","TF","TG","TH","TJ","TK","TL","TM","TN","TO","TR","TT","TV","TW","TZ","UA","UG","UM","US","UY","UZ","VA","VC","VE","VG","VI","VN","VU","WF","WS","XK","YE","YT","ZA","ZM","ZW"]},{"code":"LAC","name":"Latin America and the Caribbean","countries":["AG","AI","AR","AW","BB","BL","BO","BR","BS","CL","CO","CR","CU","CW","DM","DO","EC","GD","GF","GP","GT","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PE","PR","PY","SV","SX","TC","TT","UY","VC","VE","VG","VI"]},{"code":"LATAM","name":"Latin America","countries":["AR","BL","BO","BR","CL","CO","CR","CU","DO","EC","GF","GP","GT","HN","HT","MF","MQ","MX","NI","PA","PE","PR","PY","SV","UY","VE"]},{"code":"MAGHREB","name":"Maghreb","countries":["DZ","LY","MA","MR","TN"]},{"code":"MENA","name":"Middle East and North Africa","countries":["AE","BH","CY","DJ","DZ","EG","EH","IL","IQ","IR","JO","KW","LB","LY","MA","OM","PS","QA","SA","SD","SY","TN","TR","YE"]},{"code":"MIDEAST","name":"Middle East","countries":["AE","BH","CY","EG","IL","IQ","IR","JO","KW","LB","OM","PS","QA","SA","SY","TR","YE"]},{"code":"NAM","name":"Northern America","countries":["BM","CA","GL","PM","US"]},{"code":"NORAM","name":"North America","countries":["AG","AI","AW","BB","BL","BM","BS","BZ","CA","CR","CU","CW","DM","DO","GD","GL","GP","GT","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PM","PR","SV","SX","TC","TT","US","VC","VG","VI"]},{"code":"NORD","name":"Nordics","countries":["AX","DK","FO","FI","IS","NO","SE"]},{"code":"OCE","name":"Oceania","countries":["AS","AU","CK","FJ","FM","GU","KI","MH","MP","NC","NF","NR","NU","NZ","PF","PG","PN","PW","SB","TK","TO","TV","VU","WF","WS"]},{"code":"SAS","name":"South Asia","countries":["AF","BD","BT","IN","LK","MV","NP","PK"]},{"code":"SOUTHAM","name":"South America","countries":["AR","BO","BR","CL","CO","EC","PY","PE","UY","VE","BV","FK","GF","GY","GS","SR"]},{"code":"SSA","name":"Sub-Saharan Africa","countries":["AO","BF","BI","BJ","BW","CD","CF","CG","CI","CM","CV","DJ","ER","ET","GA","GH","GM","GN","GQ","GW","KE","KM","LR","LS","MG","ML","MR","MU","MW","MZ","NA","NE","NG","RW","SC","SD","SL","SN","SO","SS","ST","SZ","TD","TG","TZ","UG","ZA","ZM","ZW"]},{"code":"WAFR","name":"West Africa","countries":["BF","BJ","CI","CV","GH","GM","GN","GW","LR","ML","MR","NE","NG","SH","SL","SN","TG"]}]
\ No newline at end of file
+[{"code":"AFR","name":"Africa","countries":["AO","BF","BI","BJ","BW","CD","CF","CG","CI","CM","CV","DJ","DZ","EG","EH","ER","ET","GA","GH","GM","GN","GQ","GW","KE","KM","LR","LS","LY","MA","MG","ML","MR","MU","MW","MZ","NA","NE","NG","RE","RW","SC","SD","SH","SL","SN","SO","SS","ST","SZ","TD","TF","TG","TN","TZ","UG","YT","ZA","ZM","ZW"]},{"code":"AMER","name":"Americas","countries":["AG","AI","AR","AW","BB","BL","BM","BO","BR","BS","BV","BZ","CA","CL","CO","CR","CU","CW","DM","DO","EC","FK","GD","GF","GL","GP","GS","GT","GY","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PE","PM","PR","PY","SR","SV","SX","TC","TT","US","UY","VC","VE","VG","VI"]},{"code":"APAC","name":"Asia-Pacific","countries":["AF","AS","AU","BD","BN","BT","CK","CN","FJ","FM","GU","ID","IN","JP","KH","KI","KP","KR","LA","LK","MH","MM","MN","MP","MV","MY","NC","NF","NP","NR","NU","NZ","PF","PG","PH","PK","PN","PW","SB","SG","TH","TK","TL","TO","TV","TW","VN","VU","WF","WS"]},{"code":"ARAB","name":"Arab world","countries":["AE","BH","DJ","DZ","EG","IQ","JO","KM","KW","LB","LY","MA","MR","OM","PS","QA","SA","SD","SO","SY","TN","YE"]},{"code":"ASEAN","name":"Association of Southeast Asian Nations","countries":["BN","KH","ID","LA","MY","MM","PH","SG","TH","VN"]},{"code":"ASIA","name":"Asia","countries":["AE","AF","AM","AZ","BD","BH","BN","BT","CN","CY","GE","ID","IL","IN","IQ","IR","JO","JP","KG","KH","KP","KR","KW","KZ","LA","LB","LK","MM","MN","MV","MY","NP","OM","PH","PK","PS","QA","RU","SA","SG","SY","TH","TJ","TL","TM","TR","TW","UZ","VN","YE"]},{"code":"CARIB","name":"Caribbean","countries":["AG","AI","AW","BB","BL","BS","CU","CW","DM","DO","GD","GP","HT","JM","KN","KY","LC","MF","MQ","MS","PR","SX","TC","TT","VC","VG","VI"]},{"code":"CAS","name":"Central Asia","countries":["KG","KZ","TJ","TM","UZ"]},{"code":"CENAMER","name":"Central America","countries":["BZ","CR","SV","GT","HN","NI","PA"]},{"code":"CIS","name":"Commonwealth of Independent States","countries":["AM","AZ","BY","KG","KZ","MD","RU","TJ","UZ"]},{"code":"EMEA","name":"Europe, the Middle East and Africa","countries":["AD","AE","AL","AM","AO","AT","AZ","BA","BE","BF","BG","BH","BI","BJ","BW","BY","CD","CF","CG","CH","CI","CM","CV","CY","CZ","DE","DJ","DK","DZ","EE","EG","EH","ER","ES","ET","FI","FR","GA","GE","GH","GM","GN","GQ","GR","GW","HR","HU","IE","IQ","IR","IS","IT","JO","KE","KM","KW","KZ","LB","LI","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MG","MK","ML","MR","MT","MU","MW","MZ","NA","NE","NG","NL","NO","OM","PL","PS","PT","QA","RE","RO","RS","RU","RW","SA","SC","SD","SE","SH","SI","SK","SL","SM","SN","SO","SS","ST","SY","SZ","TD","TF","TG","TN","TR","TZ","UA","UG","UK","VA","YE","YT","ZA","ZM","ZW"]},{"code":"EUR","name":"Europe","countries":["AD","AL","AM","AT","AZ","BA","BE","BG","BY","CH","CY","CZ","DE","DK","EE","ES","FI","FR","GE","GR","HR","HU","IE","IS","IT","KZ","LI","LT","LU","LV","MC","MD","ME","MK","MT","NL","NO","PL","PT","RO","RS","RU","SE","SI","SK","SM","TR","UA","UK","VA"]},{"code":"HISPAM","name":"Hispanic America","countries":["AR","BO","CL","CO","CR","CU","DO","EC","GT","HN","MX","NI","PA","PE","PR","PY","SV","UY","VE"]},{"code":"LAC","name":"Latin America and the Caribbean","countries":["AG","AI","AR","AW","BB","BL","BO","BR","BS","CL","CO","CR","CU","CW","DM","DO","EC","GD","GF","GP","GT","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PE","PR","PY","SV","SX","TC","TT","UY","VC","VE","VG","VI"]},{"code":"LATAM","name":"Latin America","countries":["AR","BL","BO","BR","CL","CO","CR","CU","DO","EC","GF","GP","GT","HN","HT","MF","MQ","MX","NI","PA","PE","PR","PY","SV","UY","VE"]},{"code":"MAGHREB","name":"Maghreb","countries":["DZ","LY","MA","MR","TN"]},{"code":"MENA","name":"Middle East and North Africa","countries":["AE","BH","CY","DJ","DZ","EG","EH","IL","IQ","IR","JO","KW","LB","LY","MA","OM","PS","QA","SA","SD","SY","TN","TR","YE"]},{"code":"MIDEAST","name":"Middle East","countries":["AE","BH","CY","EG","IL","IQ","IR","JO","KW","LB","OM","PS","QA","SA","SY","TR","YE"]},{"code":"NAM","name":"Northern America","countries":["BM","CA","GL","PM","US"]},{"code":"NORAM","name":"North America","countries":["AG","AI","AW","BB","BL","BM","BS","BZ","CA","CR","CU","CW","DM","DO","GD","GL","GP","GT","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PM","PR","SV","SX","TC","TT","US","VC","VG","VI"]},{"code":"NORD","name":"Nordics","countries":["AX","DK","FO","FI","IS","NO","SE"]},{"code":"OCE","name":"Oceania","countries":["AS","AU","CK","FJ","FM","GU","KI","MH","MP","NC","NF","NR","NU","NZ","PF","PG","PN","PW","SB","TK","TO","TV","VU","WF","WS"]},{"code":"SAS","name":"South Asia","countries":["AF","BD","BT","IN","LK","MV","NP","PK"]},{"code":"SOUTHAM","name":"South America","countries":["AR","BO","BR","CL","CO","EC","PY","PE","UY","VE","BV","FK","GF","GY","GS","SR"]},{"code":"SSA","name":"Sub-Saharan Africa","countries":["AO","BF","BI","BJ","BW","CD","CF","CG","CI","CM","CV","DJ","ER","ET","GA","GH","GM","GN","GQ","GW","KE","KM","LR","LS","MG","ML","MR","MU","MW","MZ","NA","NE","NG","RW","SC","SD","SL","SN","SO","SS","ST","SZ","TD","TG","TZ","UG","ZA","ZM","ZW"]},{"code":"WAFR","name":"West Africa","countries":["BF","BJ","CI","CV","GH","GM","GN","GW","LR","ML","MR","NE","NG","SH","SL","SN","TG"]}]
\ No newline at end of file
diff --git a/tests/__data__/input/readme_update/generators.log b/tests/__data__/input/readme_update/generators.log
index 27beaa3641..a2e36eb1fe 100644
--- a/tests/__data__/input/readme_update/generators.log
+++ b/tests/__data__/input/readme_update/generators.log
@@ -74,7 +74,6 @@
{"type":"region","filepath":"regions/oce.m3u","count":0}
{"type":"region","filepath":"regions/undefined.m3u","count":2}
{"type":"region","filepath":"regions/sas.m3u","count":1}
-{"type":"region","filepath":"regions/int.m3u","count":1}
{"type":"region","filepath":"regions/southam.m3u","count":0}
{"type":"region","filepath":"regions/ssa.m3u","count":0}
{"type":"region","filepath":"regions/wafr.m3u","count":0}
\ No newline at end of file
From f665512bfcce5517d4831874581d44210b5fe9a7 Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sat, 23 Aug 2025 17:46:51 +0300
Subject: [PATCH 03/15] Update generate.test.ts
---
tests/commands/playlist/generate.test.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/tests/commands/playlist/generate.test.ts b/tests/commands/playlist/generate.test.ts
index 5489e54caa..39f38a8f36 100644
--- a/tests/commands/playlist/generate.test.ts
+++ b/tests/commands/playlist/generate.test.ts
@@ -4,7 +4,8 @@ import { EOL } from 'node:os'
import * as fs from 'fs-extra'
import * as glob from 'glob'
-const ENV_VAR = 'cross-env STREAMS_DIR=tests/__data__/input/playlist_generate DATA_DIR=tests/__data__/input/data PUBLIC_DIR=tests/__data__/output/.gh-pages LOGS_DIR=tests/__data__/output/logs'
+const ENV_VAR =
+ 'cross-env STREAMS_DIR=tests/__data__/input/playlist_generate DATA_DIR=tests/__data__/input/data PUBLIC_DIR=tests/__data__/output/.gh-pages LOGS_DIR=tests/__data__/output/logs'
beforeEach(() => {
fs.emptyDirSync('tests/__data__/output')
@@ -26,7 +27,7 @@ describe('playlist:generate', () => {
})
playlists.forEach((filepath: string) => {
- expect(content(`tests/__data__/output/${filepath}`)).toBe(
+ expect(content(`tests/__data__/output/${filepath}`), filepath).toBe(
content(`tests/__data__/expected/playlist_generate/${filepath}`)
)
})
From 63262842e737f01c1340d5df83dd9c3448cd72de Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sat, 23 Aug 2025 17:47:03 +0300
Subject: [PATCH 04/15] Update scripts
---
scripts/commands/api/load.ts | 3 +-
scripts/core/dataLoader.ts | 9 +-
scripts/core/dataProcessor.ts | 111 +++++++++++------
scripts/generators/countriesGenerator.ts | 12 --
scripts/generators/indexCountryGenerator.ts | 20 +--
scripts/generators/indexRegionGenerator.ts | 20 +--
scripts/generators/regionsGenerator.ts | 22 ----
scripts/models/broadcastArea.ts | 102 +++++++++++++++-
scripts/models/city.ts | 78 ++++++++++++
scripts/models/country.ts | 17 ++-
scripts/models/feed.ts | 127 ++++----------------
scripts/models/index.ts | 1 +
scripts/models/region.ts | 55 +++++++--
scripts/models/stream.ts | 4 -
scripts/models/subdivision.ts | 40 +++++-
scripts/types/city.d.ts | 20 +++
scripts/types/dataLoader.d.ts | 1 +
scripts/types/dataProcessor.d.ts | 3 +
scripts/types/feed.d.ts | 8 +-
scripts/types/region.d.ts | 5 +
scripts/types/subdivision.d.ts | 4 +
21 files changed, 417 insertions(+), 245 deletions(-)
create mode 100644 scripts/models/city.ts
create mode 100644 scripts/types/city.d.ts
diff --git a/scripts/commands/api/load.ts b/scripts/commands/api/load.ts
index e4d89120a2..39cf0a2e8b 100644
--- a/scripts/commands/api/load.ts
+++ b/scripts/commands/api/load.ts
@@ -18,7 +18,8 @@ async function main() {
loader.download('logos.json'),
loader.download('timezones.json'),
loader.download('guides.json'),
- loader.download('streams.json')
+ loader.download('streams.json'),
+ loader.download('cities.json')
])
}
diff --git a/scripts/core/dataLoader.ts b/scripts/core/dataLoader.ts
index d23264769a..89b45c00c0 100644
--- a/scripts/core/dataLoader.ts
+++ b/scripts/core/dataLoader.ts
@@ -57,7 +57,8 @@ export class DataLoader {
logos,
timezones,
guides,
- streams
+ streams,
+ cities
] = await Promise.all([
this.storage.json('countries.json'),
this.storage.json('regions.json'),
@@ -70,7 +71,8 @@ export class DataLoader {
this.storage.json('logos.json'),
this.storage.json('timezones.json'),
this.storage.json('guides.json'),
- this.storage.json('streams.json')
+ this.storage.json('streams.json'),
+ this.storage.json('cities.json')
])
return {
@@ -85,7 +87,8 @@ export class DataLoader {
logos,
timezones,
guides,
- streams
+ streams,
+ cities
}
}
diff --git a/scripts/core/dataProcessor.ts b/scripts/core/dataProcessor.ts
index d36569fd53..dfb796ba70 100644
--- a/scripts/core/dataProcessor.ts
+++ b/scripts/core/dataProcessor.ts
@@ -1,3 +1,4 @@
+import { DataProcessorData } from '../types/dataProcessor'
import { DataLoaderData } from '../types/dataLoader'
import { Collection } from '@freearhey/core'
import {
@@ -11,14 +12,16 @@ import {
Region,
Stream,
Guide,
+ City,
Feed,
Logo
} from '../models'
export class DataProcessor {
- constructor() {}
+ process(data: DataLoaderData): DataProcessorData {
+ let regions = new Collection(data.regions).map(data => new Region(data))
+ let regionsKeyByCode = regions.keyBy((region: Region) => region.code)
- process(data: DataLoaderData) {
const categories = new Collection(data.categories).map(data => new Category(data))
const categoriesKeyById = categories.keyBy((category: Category) => category.id)
@@ -26,25 +29,23 @@ export class DataProcessor {
const languagesKeyByCode = languages.keyBy((language: Language) => language.code)
let subdivisions = new Collection(data.subdivisions).map(data => new Subdivision(data))
- const subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
- const subdivisionsGroupedByCountryCode = subdivisions.groupBy(
+ let subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
+ let subdivisionsGroupedByCountryCode = subdivisions.groupBy(
(subdivision: Subdivision) => subdivision.countryCode
)
- let regions = new Collection(data.regions).map(data => new Region(data))
- const regionsKeyByCode = regions.keyBy((region: Region) => region.code)
+ let countries = new Collection(data.countries).map(data => new Country(data))
+ let countriesKeyByCode = countries.keyBy((country: Country) => country.code)
- const countries = new Collection(data.countries).map(data =>
- new Country(data)
+ const cities = new Collection(data.cities).map(data =>
+ new City(data)
.withRegions(regions)
- .withLanguage(languagesKeyByCode)
- .withSubdivisions(subdivisionsGroupedByCountryCode)
- )
- const countriesKeyByCode = countries.keyBy((country: Country) => country.code)
-
- subdivisions = subdivisions.map((subdivision: Subdivision) =>
- subdivision.withCountry(countriesKeyByCode)
+ .withCountry(countriesKeyByCode)
+ .withSubdivision(subdivisionsKeyByCode)
)
+ const citiesKeyByCode = cities.keyBy((city: City) => city.code)
+ const citiesGroupedByCountryCode = cities.groupBy((city: City) => city.countryCode)
+ const citiesGroupedBySubdivisionCode = cities.groupBy((city: City) => city.subdivisionCode)
const timezones = new Collection(data.timezones).map(data =>
new Timezone(data).withCountries(countriesKeyByCode)
@@ -56,27 +57,12 @@ export class DataProcessor {
(blocklistRecord: BlocklistRecord) => blocklistRecord.channelId
)
- let channels = new Collection(data.channels).map(data =>
- new Channel(data)
- .withCategories(categoriesKeyById)
- .withCountry(countriesKeyByCode)
- .withSubdivision(subdivisionsKeyByCode)
- .withCategories(categoriesKeyById)
- )
+ let channels = new Collection(data.channels).map(data => new Channel(data))
+ let channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
- const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
-
- const feeds = new Collection(data.feeds).map(data =>
- new Feed(data)
- .withChannel(channelsKeyById)
- .withLanguages(languagesKeyByCode)
- .withTimezones(timezonesKeyById)
- .withBroadcastCountries(countriesKeyByCode, regionsKeyByCode, subdivisionsKeyByCode)
- .withBroadcastRegions(regions)
- .withBroadcastSubdivisions(subdivisionsKeyByCode)
- )
- const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
- const feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
+ let feeds = new Collection(data.feeds).map(data => new Feed(data))
+ let feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
+ let feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
const logos = new Collection(data.logos).map(data => new Logo(data).withFeed(feedsGroupedById))
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
@@ -90,11 +76,60 @@ export class DataProcessor {
const guides = new Collection(data.guides).map(data => new Guide(data))
const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId())
- regions = regions.map((region: Region) => region.withCountries(countriesKeyByCode))
+ regions = regions.map((region: Region) =>
+ region
+ .withCountries(countriesKeyByCode)
+ .withRegions(regions)
+ .withSubdivisions(subdivisions)
+ .withCities(cities)
+ )
+ regionsKeyByCode = regions.keyBy((region: Region) => region.code)
+
+ countries = countries.map((country: Country) =>
+ country
+ .withCities(citiesGroupedByCountryCode)
+ .withSubdivisions(subdivisionsGroupedByCountryCode)
+ .withRegions(regions)
+ .withLanguage(languagesKeyByCode)
+ )
+ countriesKeyByCode = countries.keyBy((country: Country) => country.code)
+
+ subdivisions = subdivisions.map((subdivision: Subdivision) =>
+ subdivision
+ .withCities(citiesGroupedBySubdivisionCode)
+ .withCountry(countriesKeyByCode)
+ .withRegions(regions)
+ )
+ subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
+ subdivisionsGroupedByCountryCode = subdivisions.groupBy(
+ (subdivision: Subdivision) => subdivision.countryCode
+ )
channels = channels.map((channel: Channel) =>
- channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
+ channel
+ .withFeeds(feedsGroupedByChannelId)
+ .withLogos(logosGroupedByChannelId)
+ .withCategories(categoriesKeyById)
+ .withCountry(countriesKeyByCode)
+ .withSubdivision(subdivisionsKeyByCode)
+ .withCategories(categoriesKeyById)
)
+ channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
+
+ feeds = feeds.map((feed: Feed) =>
+ feed
+ .withChannel(channelsKeyById)
+ .withLanguages(languagesKeyByCode)
+ .withTimezones(timezonesKeyById)
+ .withBroadcastArea(
+ citiesKeyByCode,
+ subdivisionsKeyByCode,
+ countriesKeyByCode,
+ regionsKeyByCode
+ )
+ )
+ feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
+ feedsGroupedById = feeds.groupBy((feed: Feed) => feed.id)
return {
blocklistRecordsGroupedByChannelId,
@@ -111,6 +146,7 @@ export class DataProcessor {
regionsKeyByCode,
blocklistRecords,
channelsKeyById,
+ citiesKeyByCode,
subdivisions,
categories,
countries,
@@ -119,6 +155,7 @@ export class DataProcessor {
channels,
regions,
streams,
+ cities,
guides,
feeds,
logos
diff --git a/scripts/generators/countriesGenerator.ts b/scripts/generators/countriesGenerator.ts
index 4cd539deaf..2cd5cab760 100644
--- a/scripts/generators/countriesGenerator.ts
+++ b/scripts/generators/countriesGenerator.ts
@@ -40,17 +40,5 @@ export class CountriesGenerator implements Generator {
JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() }) + EOL
)
})
-
- const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea())
- const undefinedPlaylist = new Playlist(undefinedStreams, { public: true })
- const undefinedFilepath = 'countries/undefined.m3u'
- await this.storage.save(undefinedFilepath, undefinedPlaylist.toString())
- this.logFile.append(
- JSON.stringify({
- type: 'country',
- filepath: undefinedFilepath,
- count: undefinedPlaylist.streams.count()
- }) + EOL
- )
}
}
diff --git a/scripts/generators/indexCountryGenerator.ts b/scripts/generators/indexCountryGenerator.ts
index 43e55c2e28..c7e5e2f848 100644
--- a/scripts/generators/indexCountryGenerator.ts
+++ b/scripts/generators/indexCountryGenerator.ts
@@ -26,18 +26,7 @@ export class IndexCountryGenerator implements Generator {
.orderBy((stream: Stream) => stream.getTitle())
.filter((stream: Stream) => stream.isSFW())
.forEach((stream: Stream) => {
- if (!stream.hasBroadcastArea()) {
- const streamClone = stream.clone()
- streamClone.groupTitle = 'Undefined'
- groupedStreams.add(streamClone)
- return
- }
-
- if (stream.isInternational()) {
- const streamClone = stream.clone()
- streamClone.groupTitle = 'International'
- groupedStreams.add(streamClone)
- }
+ if (!stream.hasBroadcastArea()) return
stream.getBroadcastCountries().forEach((country: Country) => {
const streamClone = stream.clone()
@@ -46,12 +35,7 @@ export class IndexCountryGenerator implements Generator {
})
})
- groupedStreams = groupedStreams.orderBy((stream: Stream) => {
- if (stream.groupTitle === 'International') return 'ZZ'
- if (stream.groupTitle === 'Undefined') return 'ZZZ'
-
- return stream.groupTitle
- })
+ groupedStreams = groupedStreams.orderBy((stream: Stream) => stream.groupTitle)
const playlist = new Playlist(groupedStreams, { public: true })
const filepath = 'index.country.m3u'
diff --git a/scripts/generators/indexRegionGenerator.ts b/scripts/generators/indexRegionGenerator.ts
index 3155d37d30..43775ec91e 100644
--- a/scripts/generators/indexRegionGenerator.ts
+++ b/scripts/generators/indexRegionGenerator.ts
@@ -28,19 +28,7 @@ export class IndexRegionGenerator implements Generator {
.orderBy((stream: Stream) => stream.getTitle())
.filter((stream: Stream) => stream.isSFW())
.forEach((stream: Stream) => {
- if (stream.isInternational()) {
- const streamClone = stream.clone()
- streamClone.groupTitle = 'International'
- groupedStreams.push(streamClone)
- return
- }
-
- if (!stream.hasBroadcastArea()) {
- const streamClone = stream.clone()
- streamClone.groupTitle = 'Undefined'
- groupedStreams.push(streamClone)
- return
- }
+ if (!stream.hasBroadcastArea()) return
stream.getBroadcastRegions().forEach((region: Region) => {
const streamClone = stream.clone()
@@ -49,11 +37,7 @@ export class IndexRegionGenerator implements Generator {
})
})
- groupedStreams = groupedStreams.orderBy((stream: Stream) => {
- if (stream.groupTitle === 'International') return 'ZZ'
- if (stream.groupTitle === 'Undefined') return 'ZZZ'
- return stream.groupTitle
- })
+ groupedStreams = groupedStreams.orderBy((stream: Stream) => stream.groupTitle)
const playlist = new Playlist(groupedStreams, { public: true })
const filepath = 'index.region.m3u'
diff --git a/scripts/generators/regionsGenerator.ts b/scripts/generators/regionsGenerator.ts
index 6711c67ff8..02112974ed 100644
--- a/scripts/generators/regionsGenerator.ts
+++ b/scripts/generators/regionsGenerator.ts
@@ -28,8 +28,6 @@ export class RegionsGenerator implements Generator {
.filter((stream: Stream) => stream.isSFW())
this.regions.forEach(async (region: Region) => {
- if (region.isWorldwide()) return
-
const regionStreams = streams.filter((stream: Stream) => stream.isBroadcastInRegion(region))
const playlist = new Playlist(regionStreams, { public: true })
@@ -39,25 +37,5 @@ export class RegionsGenerator implements Generator {
JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL
)
})
-
- const internationalStreams = streams.filter((stream: Stream) => stream.isInternational())
- const internationalPlaylist = new Playlist(internationalStreams, { public: true })
- const internationalFilepath = 'regions/int.m3u'
- await this.storage.save(internationalFilepath, internationalPlaylist.toString())
- this.logFile.append(
- JSON.stringify({
- type: 'region',
- filepath: internationalFilepath,
- count: internationalPlaylist.streams.count()
- }) + EOL
- )
-
- const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea())
- const playlist = new Playlist(undefinedStreams, { public: true })
- const filepath = 'regions/undefined.m3u'
- await this.storage.save(filepath, playlist.toString())
- this.logFile.append(
- JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL
- )
}
}
diff --git a/scripts/models/broadcastArea.ts b/scripts/models/broadcastArea.ts
index 2b96b7f91f..8949b375dd 100644
--- a/scripts/models/broadcastArea.ts
+++ b/scripts/models/broadcastArea.ts
@@ -1,11 +1,101 @@
-type BroadcastAreaProps = {
- code: string
-}
+import { Collection, Dictionary } from '@freearhey/core'
+import { City, Subdivision, Region, Country } from './'
export class BroadcastArea {
- code: string
+ codes: Collection
+ citiesIncluded: Collection
+ subdivisionsIncluded: Collection
+ countriesIncluded: Collection
+ regionsIncluded: Collection
- constructor(data: BroadcastAreaProps) {
- this.code = data.code
+ constructor(codes: Collection) {
+ this.codes = codes
+ }
+
+ withLocations(
+ citiesKeyByCode: Dictionary,
+ subdivisionsKeyByCode: Dictionary,
+ countriesKeyByCode: Dictionary,
+ regionsKeyByCode: Dictionary
+ ): this {
+ let citiesIncluded = new Collection()
+ let subdivisionsIncluded = new Collection()
+ let countriesIncluded = new Collection()
+ let regionsIncluded = new Collection()
+
+ this.codes.forEach((value: string) => {
+ const [type, code] = value.split('/')
+
+ switch (type) {
+ case 'ct': {
+ const city: City = citiesKeyByCode.get(code)
+ if (!city) return
+ citiesIncluded.add(city)
+ }
+ case 's': {
+ const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
+ if (!subdivision) return
+ citiesIncluded = citiesIncluded.concat(subdivision.getCities())
+ subdivisionsIncluded.add(subdivision)
+ }
+ case 'c': {
+ const country: Country = countriesKeyByCode.get(code)
+ if (!country) return
+ citiesIncluded = citiesIncluded.concat(country.getCities())
+ subdivisionsIncluded = subdivisionsIncluded.concat(country.getSubdivisions())
+ countriesIncluded.add(country)
+ regionsIncluded = regionsIncluded.concat(country.getRegions())
+ }
+ case 'r': {
+ const region: Region = regionsKeyByCode.get(code)
+ if (!region) return
+ countriesIncluded = countriesIncluded.concat(region.getCountries())
+ regionsIncluded = regionsIncluded.concat(region.getRegions())
+ }
+ }
+ })
+
+ this.citiesIncluded = citiesIncluded.uniqBy((city: City) => city.code)
+ this.subdivisionsIncluded = subdivisionsIncluded.uniqBy(
+ (subdivision: Subdivision) => subdivision.code
+ )
+ this.countriesIncluded = countriesIncluded.uniqBy((country: Country) => country.code)
+ this.regionsIncluded = regionsIncluded.uniqBy((region: Region) => region.code)
+
+ return this
+ }
+
+ getCountries(): Collection {
+ return this.countriesIncluded || new Collection()
+ }
+
+ getSubdivisions(): Collection {
+ return this.subdivisionsIncluded || new Collection()
+ }
+
+ getCities(): Collection {
+ return this.citiesIncluded || new Collection()
+ }
+
+ getRegions(): Collection {
+ return this.regionsIncluded || new Collection()
+ }
+
+ includesCountry(country: Country): boolean {
+ return this.getCountries().includes((_country: Country) => _country.code === country.code)
+ }
+
+ includesSubdivision(subdivision: Subdivision): boolean {
+ return this.getSubdivisions().includes(
+ (_subdivision: Subdivision) => _subdivision.code === subdivision.code
+ )
+ }
+
+ includesRegion(region: Region): boolean {
+ return this.getRegions().includes((_region: Region) => _region.code === region.code)
+ }
+
+ includesCity(city: City): boolean {
+ return this.getCities().includes((_city: City) => _city.code === city.code)
}
}
diff --git a/scripts/models/city.ts b/scripts/models/city.ts
new file mode 100644
index 0000000000..6ce9173ac9
--- /dev/null
+++ b/scripts/models/city.ts
@@ -0,0 +1,78 @@
+import { Collection, Dictionary } from '@freearhey/core'
+import { Country, Region, Subdivision } from '.'
+import type { CityData, CitySerializedData } from '../types/city'
+
+export class City {
+ code: string
+ name: string
+ countryCode: string
+ country?: Country
+ subdivisionCode?: string
+ subdivision?: Subdivision
+ wikidataId: string
+ regions?: Collection
+
+ constructor(data?: CityData) {
+ if (!data) return
+
+ this.code = data.code
+ this.name = data.name
+ this.countryCode = data.country
+ this.subdivisionCode = data.subdivision || undefined
+ this.wikidataId = data.wikidata_id
+ }
+
+ withCountry(countriesKeyByCode: Dictionary): this {
+ this.country = countriesKeyByCode.get(this.countryCode)
+
+ return this
+ }
+
+ withSubdivision(subdivisionsKeyByCode: Dictionary): this {
+ if (!this.subdivisionCode) return this
+
+ this.subdivision = subdivisionsKeyByCode.get(this.subdivisionCode)
+
+ return this
+ }
+
+ withRegions(regions: Collection): this {
+ this.regions = regions.filter((region: Region) =>
+ region.countryCodes.includes(this.countryCode)
+ )
+
+ return this
+ }
+
+ getRegions(): Collection {
+ if (!this.regions) return new Collection()
+
+ return this.regions
+ }
+
+ serialize(): CitySerializedData {
+ return {
+ code: this.code,
+ name: this.name,
+ countryCode: this.countryCode,
+ country: this.country ? this.country.serialize() : undefined,
+ subdivisionCode: this.subdivisionCode || null,
+ subdivision: this.subdivision ? this.subdivision.serialize() : undefined,
+ wikidataId: this.wikidataId
+ }
+ }
+
+ deserialize(data: CitySerializedData): this {
+ this.code = data.code
+ this.name = data.name
+ this.countryCode = data.countryCode
+ this.country = data.country ? new Country().deserialize(data.country) : undefined
+ this.subdivisionCode = data.subdivisionCode || undefined
+ this.subdivision = data.subdivision
+ ? new Subdivision().deserialize(data.subdivision)
+ : undefined
+ this.wikidataId = data.wikidataId
+
+ return this
+ }
+}
diff --git a/scripts/models/country.ts b/scripts/models/country.ts
index 780c4413f1..b9699f7235 100644
--- a/scripts/models/country.ts
+++ b/scripts/models/country.ts
@@ -12,6 +12,7 @@ export class Country {
language?: Language
subdivisions?: Collection
regions?: Collection
+ cities?: Collection
constructor(data?: CountryData) {
if (!data) return
@@ -23,15 +24,19 @@ export class Country {
}
withSubdivisions(subdivisionsGroupedByCountryCode: Dictionary): this {
- this.subdivisions = subdivisionsGroupedByCountryCode.get(this.code) || new Collection()
+ this.subdivisions = new Collection(subdivisionsGroupedByCountryCode.get(this.code))
return this
}
withRegions(regions: Collection): this {
- this.regions = regions.filter(
- (region: Region) => region.code !== 'INT' && region.includesCountryCode(this.code)
- )
+ this.regions = regions.filter((region: Region) => region.includesCountryCode(this.code))
+
+ return this
+ }
+
+ withCities(citiesGroupedByCountryCode: Dictionary): this {
+ this.cities = new Collection(citiesGroupedByCountryCode.get(this.code))
return this
}
@@ -54,6 +59,10 @@ export class Country {
return this.subdivisions || new Collection()
}
+ getCities(): Collection {
+ return this.cities || new Collection()
+ }
+
serialize(): CountrySerializedData {
return {
code: this.code,
diff --git a/scripts/models/feed.ts b/scripts/models/feed.ts
index f42c4af916..7515fdc652 100644
--- a/scripts/models/feed.ts
+++ b/scripts/models/feed.ts
@@ -1,4 +1,4 @@
-import { Country, Language, Region, Channel, Subdivision } from './index'
+import { Country, Language, Region, Channel, Subdivision, BroadcastArea } from './index'
import { Collection, Dictionary } from '@freearhey/core'
import type { FeedData } from '../types/feed'
@@ -9,12 +9,7 @@ export class Feed {
name: string
isMain: boolean
broadcastAreaCodes: Collection
- broadcastCountryCodes: Collection
- broadcastCountries?: Collection
- broadcastRegionCodes: Collection
- broadcastRegions?: Collection
- broadcastSubdivisionCodes: Collection
- broadcastSubdivisions?: Collection
+ broadcastArea?: BroadcastArea
languageCodes: Collection
languages?: Collection
timezoneIds: Collection
@@ -32,25 +27,6 @@ export class Feed {
this.languageCodes = new Collection(data.languages)
this.timezoneIds = new Collection(data.timezones)
this.videoFormat = data.video_format
- this.broadcastCountryCodes = new Collection()
- this.broadcastRegionCodes = new Collection()
- this.broadcastSubdivisionCodes = new Collection()
-
- this.broadcastAreaCodes.forEach((areaCode: string) => {
- const [type, code] = areaCode.split('/')
-
- switch (type) {
- case 'c':
- this.broadcastCountryCodes.add(code)
- break
- case 'r':
- this.broadcastRegionCodes.add(code)
- break
- case 's':
- this.broadcastSubdivisionCodes.add(code)
- break
- }
- })
}
withChannel(channelsKeyById: Dictionary): this {
@@ -93,76 +69,36 @@ export class Feed {
return this
}
- withBroadcastSubdivisions(subdivisionsKeyByCode: Dictionary): this {
- this.broadcastSubdivisions = this.broadcastSubdivisionCodes.map((code: string) =>
- subdivisionsKeyByCode.get(code)
- )
-
- return this
- }
-
- withBroadcastCountries(
+ withBroadcastArea(
+ citiesKeyByCode: Dictionary,
+ subdivisionsKeyByCode: Dictionary,
countriesKeyByCode: Dictionary,
- regionsKeyByCode: Dictionary,
- subdivisionsKeyByCode: Dictionary
+ regionsKeyByCode: Dictionary
): this {
- const broadcastCountries = new Collection()
-
- if (this.isInternational()) {
- this.broadcastCountries = broadcastCountries
- return this
- }
-
- this.broadcastCountryCodes.forEach((code: string) => {
- broadcastCountries.add(countriesKeyByCode.get(code))
- })
-
- this.broadcastRegionCodes.forEach((code: string) => {
- const region: Region = regionsKeyByCode.get(code)
- if (region) {
- region.countryCodes.forEach((countryCode: string) => {
- broadcastCountries.add(countriesKeyByCode.get(countryCode))
- })
- }
- })
-
- this.broadcastSubdivisionCodes.forEach((code: string) => {
- const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
- if (subdivision) {
- broadcastCountries.add(countriesKeyByCode.get(subdivision.countryCode))
- }
- })
-
- this.broadcastCountries = broadcastCountries.uniq().filter(Boolean)
-
- return this
- }
-
- withBroadcastRegions(regions: Collection): this {
- if (!this.broadcastCountries) return this
- const countriesCodes = this.broadcastCountries.map((country: Country) => country.code)
-
- this.broadcastRegions = regions.filter((region: Region) => {
- if (region.code === 'INT') return false
- const intersected = region.countryCodes.intersects(countriesCodes)
- return intersected.notEmpty()
- })
+ this.broadcastArea = new BroadcastArea(this.broadcastAreaCodes).withLocations(
+ citiesKeyByCode,
+ subdivisionsKeyByCode,
+ countriesKeyByCode,
+ regionsKeyByCode
+ )
return this
}
hasBroadcastArea(): boolean {
- return (
- this.isInternational() || (!!this.broadcastCountries && this.broadcastCountries.notEmpty())
- )
+ return !!this.broadcastArea
}
getBroadcastCountries(): Collection {
- return this.broadcastCountries || new Collection()
+ if (!this.broadcastArea) return new Collection()
+
+ return this.broadcastArea.getCountries()
}
getBroadcastRegions(): Collection {
- return this.broadcastRegions || new Collection()
+ if (!this.broadcastArea) return new Collection()
+
+ return this.broadcastArea.getRegions()
}
getTimezones(): Collection {
@@ -184,35 +120,22 @@ export class Feed {
)
}
- isInternational(): boolean {
- return this.broadcastAreaCodes.includes('r/INT')
- }
-
isBroadcastInSubdivision(subdivision: Subdivision): boolean {
- if (this.isInternational()) return false
- if (this.broadcastSubdivisionCodes.includes(subdivision.code)) return true
- if (
- this.broadcastSubdivisionCodes.isEmpty() &&
- subdivision.country &&
- this.isBroadcastInCountry(subdivision.country)
- )
- return true
+ if (!this.broadcastArea) return false
- return false
+ return this.broadcastArea.includesSubdivision(subdivision)
}
isBroadcastInCountry(country: Country): boolean {
- if (this.isInternational()) return false
+ if (!this.broadcastArea) return false
- return this.getBroadcastCountries().includes(
- (_country: Country) => _country.code === country.code
- )
+ return this.broadcastArea?.includesCountry(country)
}
isBroadcastInRegion(region: Region): boolean {
- if (this.isInternational()) return false
+ if (!this.broadcastArea) return false
- return this.getBroadcastRegions().includes((_region: Region) => _region.code === region.code)
+ return this.broadcastArea?.includesRegion(region)
}
getGuides(): Collection {
diff --git a/scripts/models/index.ts b/scripts/models/index.ts
index 4e11d28b97..e8fe346289 100644
--- a/scripts/models/index.ts
+++ b/scripts/models/index.ts
@@ -2,6 +2,7 @@ export * from './blocklistRecord'
export * from './broadcastArea'
export * from './category'
export * from './channel'
+export * from './city'
export * from './country'
export * from './feed'
export * from './guide'
diff --git a/scripts/models/region.ts b/scripts/models/region.ts
index ace44bc52f..35e0baa78a 100644
--- a/scripts/models/region.ts
+++ b/scripts/models/region.ts
@@ -1,15 +1,18 @@
import { Collection, Dictionary } from '@freearhey/core'
-import { Country, Subdivision } from '.'
+import { City, Country, Subdivision } from '.'
import type { RegionData, RegionSerializedData } from '../types/region'
import { CountrySerializedData } from '../types/country'
import { SubdivisionSerializedData } from '../types/subdivision'
+import { CitySerializedData } from '../types/city'
export class Region {
code: string
name: string
countryCodes: Collection
- countries: Collection = new Collection()
- subdivisions: Collection = new Collection()
+ countries?: Collection
+ subdivisions?: Collection
+ cities?: Collection
+ regions?: Collection
constructor(data?: RegionData) {
if (!data) return
@@ -33,30 +36,61 @@ export class Region {
return this
}
+ withCities(cities: Collection): this {
+ this.cities = cities.filter((city: City) => this.countryCodes.indexOf(city.countryCode) > -1)
+
+ return this
+ }
+
+ withRegions(regions: Collection): this {
+ this.regions = regions.filter(
+ (region: Region) => !region.countryCodes.intersects(this.countryCodes).isEmpty()
+ )
+
+ return this
+ }
+
getSubdivisions(): Collection {
+ if (!this.subdivisions) return new Collection()
+
return this.subdivisions
}
getCountries(): Collection {
+ if (!this.countries) return new Collection()
+
return this.countries
}
+ getCities(): Collection {
+ if (!this.cities) return new Collection()
+
+ return this.cities
+ }
+
+ getRegions(): Collection {
+ if (!this.regions) return new Collection()
+
+ return this.regions
+ }
+
includesCountryCode(code: string): boolean {
return this.countryCodes.includes((countryCode: string) => countryCode === code)
}
- isWorldwide(): boolean {
- return this.code === 'INT'
- }
-
serialize(): RegionSerializedData {
return {
code: this.code,
name: this.name,
countryCodes: this.countryCodes.all(),
- countries: this.countries.map((country: Country) => country.serialize()).all(),
- subdivisions: this.subdivisions
+ countries: this.getCountries()
+ .map((country: Country) => country.serialize())
+ .all(),
+ subdivisions: this.getSubdivisions()
.map((subdivision: Subdivision) => subdivision.serialize())
+ .all(),
+ cities: this.getCities()
+ .map((city: City) => city.serialize())
.all()
}
}
@@ -71,6 +105,9 @@ export class Region {
this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) =>
new Subdivision().deserialize(data)
)
+ this.cities = new Collection(data.cities).map((data: CitySerializedData) =>
+ new City().deserialize(data)
+ )
return this
}
diff --git a/scripts/models/stream.ts b/scripts/models/stream.ts
index cd734493b8..395e179207 100644
--- a/scripts/models/stream.ts
+++ b/scripts/models/stream.ts
@@ -342,10 +342,6 @@ export class Stream {
return this.feed ? this.feed.isBroadcastInRegion(region) : false
}
- isInternational(): boolean {
- return this.feed ? this.feed.isInternational() : false
- }
-
getLogos(): Collection {
function format(logo: Logo): number {
const levelByFormat = { SVG: 0, PNG: 3, APNG: 1, WebP: 1, AVIF: 1, JPEG: 2, GIF: 1 }
diff --git a/scripts/models/subdivision.ts b/scripts/models/subdivision.ts
index b43d1c88d7..92cfdc9d61 100644
--- a/scripts/models/subdivision.ts
+++ b/scripts/models/subdivision.ts
@@ -1,12 +1,15 @@
import { SubdivisionData, SubdivisionSerializedData } from '../types/subdivision'
-import { Dictionary } from '@freearhey/core'
-import { Country } from '.'
+import { Dictionary, Collection } from '@freearhey/core'
+import { Country, Region } from '.'
export class Subdivision {
code: string
name: string
countryCode: string
country?: Country
+ parentCode?: string
+ regions?: Collection
+ cities?: Collection
constructor(data?: SubdivisionData) {
if (!data) return
@@ -14,6 +17,7 @@ export class Subdivision {
this.code = data.code
this.name = data.name
this.countryCode = data.country
+ this.parentCode = data.parent || undefined
}
withCountry(countriesKeyByCode: Dictionary): this {
@@ -22,12 +26,39 @@ export class Subdivision {
return this
}
+ withRegions(regions: Collection): this {
+ this.regions = regions.filter((region: Region) =>
+ region.countryCodes.includes(this.countryCode)
+ )
+
+ return this
+ }
+
+ withCities(citiesGroupedBySubdivisionCode: Dictionary): this {
+ this.cities = new Collection(citiesGroupedBySubdivisionCode.get(this.code))
+
+ return this
+ }
+
+ getRegions(): Collection {
+ if (!this.regions) return new Collection()
+
+ return this.regions
+ }
+
+ getCities(): Collection {
+ if (!this.cities) return new Collection()
+
+ return this.cities
+ }
+
serialize(): SubdivisionSerializedData {
return {
code: this.code,
name: this.name,
- countryCode: this.code,
- country: this.country ? this.country.serialize() : undefined
+ countryCode: this.countryCode,
+ country: this.country ? this.country.serialize() : undefined,
+ parentCode: this.parentCode || null
}
}
@@ -36,6 +67,7 @@ export class Subdivision {
this.name = data.name
this.countryCode = data.countryCode
this.country = data.country ? new Country().deserialize(data.country) : undefined
+ this.parentCode = data.parentCode || undefined
return this
}
diff --git a/scripts/types/city.d.ts b/scripts/types/city.d.ts
new file mode 100644
index 0000000000..5c33ba5a9e
--- /dev/null
+++ b/scripts/types/city.d.ts
@@ -0,0 +1,20 @@
+import { CountrySerializedData } from './country'
+import { SubdivisionSerializedData } from './subdivision'
+
+export type CitySerializedData = {
+ code: string
+ name: string
+ countryCode: string
+ country?: CountrySerializedData
+ subdivisionCode: string | null
+ subdivision?: SubdivisionSerializedData
+ wikidataId: string
+}
+
+export type CityData = {
+ code: string
+ name: string
+ country: string
+ subdivision: string | null
+ wikidata_id: string
+}
diff --git a/scripts/types/dataLoader.d.ts b/scripts/types/dataLoader.d.ts
index ee7ab0f937..708361de99 100644
--- a/scripts/types/dataLoader.d.ts
+++ b/scripts/types/dataLoader.d.ts
@@ -17,4 +17,5 @@ export type DataLoaderData = {
timezones: object | object[]
guides: object | object[]
streams: object | object[]
+ cities: object | object[]
}
diff --git a/scripts/types/dataProcessor.d.ts b/scripts/types/dataProcessor.d.ts
index 25f21d1aac..bc76dc28b4 100644
--- a/scripts/types/dataProcessor.d.ts
+++ b/scripts/types/dataProcessor.d.ts
@@ -15,6 +15,7 @@ export type DataProcessorData = {
regionsKeyByCode: Dictionary
blocklistRecords: Collection
channelsKeyById: Dictionary
+ citiesKeyByCode: Dictionary
subdivisions: Collection
categories: Collection
countries: Collection
@@ -23,6 +24,8 @@ export type DataProcessorData = {
channels: Collection
regions: Collection
streams: Collection
+ cities: Collection
guides: Collection
feeds: Collection
+ logos: Collection
}
diff --git a/scripts/types/feed.d.ts b/scripts/types/feed.d.ts
index 5c6722dde2..ef4aea4669 100644
--- a/scripts/types/feed.d.ts
+++ b/scripts/types/feed.d.ts
@@ -1,12 +1,10 @@
-import { Collection } from '@freearhey/core'
-
export type FeedData = {
channel: string
id: string
name: string
is_main: boolean
- broadcast_area: Collection
- languages: Collection
- timezones: Collection
+ broadcast_area: string[]
+ languages: string[]
+ timezones: string[]
video_format: string
}
diff --git a/scripts/types/region.d.ts b/scripts/types/region.d.ts
index e6773429ee..798224ee7f 100644
--- a/scripts/types/region.d.ts
+++ b/scripts/types/region.d.ts
@@ -1,9 +1,14 @@
+import { CitySerializedData } from './city'
+import { CountrySerializedData } from './country'
+import { SubdivisionSerializedData } from './subdivision'
+
export type RegionSerializedData = {
code: string
name: string
countryCodes: string[]
countries?: CountrySerializedData[]
subdivisions?: SubdivisionSerializedData[]
+ cities?: CitySerializedData[]
}
export type RegionData = {
diff --git a/scripts/types/subdivision.d.ts b/scripts/types/subdivision.d.ts
index bf46831f72..b2a25982dd 100644
--- a/scripts/types/subdivision.d.ts
+++ b/scripts/types/subdivision.d.ts
@@ -1,12 +1,16 @@
+import { CountrySerializedData } from './country'
+
export type SubdivisionSerializedData = {
code: string
name: string
countryCode: string
country?: CountrySerializedData
+ parentCode: string | null
}
export type SubdivisionData = {
code: string
name: string
country: string
+ parent: string | null
}
From 3a1999e96cd74bad0ea3d732b19617c65a91918c Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sat, 23 Aug 2025 19:24:55 +0300
Subject: [PATCH 05/15] Update tests/__data__
---
.../playlist_generate/.gh-pages/subdivisions/ad-02.m3u | 3 +++
.../playlist_generate/.gh-pages/subdivisions/ca-on.m3u | 2 ++
tests/__data__/expected/playlist_generate/logs/generators.log | 3 ++-
3 files changed, 7 insertions(+), 1 deletion(-)
create mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ad-02.m3u
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ad-02.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ad-02.m3u
new file mode 100644
index 0000000000..92a59e8a10
--- /dev/null
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ad-02.m3u
@@ -0,0 +1,3 @@
+#EXTM3U
+#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Undefined",ATV
+https://iptv-all.lanesh4d0w.repl.co/andorra/atv
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u
index 754a6969c0..8aeaaae90e 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u
@@ -1,3 +1,5 @@
#EXTM3U
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
+#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
+http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/logs/generators.log b/tests/__data__/expected/playlist_generate/logs/generators.log
index 903c4022f8..557bf67dbb 100644
--- a/tests/__data__/expected/playlist_generate/logs/generators.log
+++ b/tests/__data__/expected/playlist_generate/logs/generators.log
@@ -44,6 +44,7 @@
{"type":"country","filepath":"countries/tm.m3u","count":1}
{"type":"country","filepath":"countries/kg.m3u","count":1}
{"type":"country","filepath":"countries/uz.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ad-02.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-ab.m3u","count":1}
{"type":"region","filepath":"regions/afr.m3u","count":0}
{"type":"subdivision","filepath":"subdivisions/ca-nb.m3u","count":1}
@@ -51,7 +52,7 @@
{"type":"subdivision","filepath":"subdivisions/ca-bc.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-nt.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-nl.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":2}
{"type":"subdivision","filepath":"subdivisions/ca-qc.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-ns.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-nu.m3u","count":1}
From 3105f9b1c32b2b3087f059e6f6c769960ff7eabe Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sat, 23 Aug 2025 19:24:58 +0300
Subject: [PATCH 06/15] Update broadcastArea.ts
---
scripts/models/broadcastArea.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/scripts/models/broadcastArea.ts b/scripts/models/broadcastArea.ts
index 8949b375dd..cd74efcefd 100644
--- a/scripts/models/broadcastArea.ts
+++ b/scripts/models/broadcastArea.ts
@@ -31,6 +31,7 @@ export class BroadcastArea {
const city: City = citiesKeyByCode.get(code)
if (!city) return
citiesIncluded.add(city)
+ if (city.subdivision) subdivisionsIncluded.add(city.subdivision)
}
case 's': {
const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
From dc4b4e706dc01b6cac7bd0ad4f51321f156b6d4f Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sat, 23 Aug 2025 19:48:49 +0300
Subject: [PATCH 07/15] Update tests/__data__
---
.../.gh-pages/countries/undefined.m3u | 15 +++++++++++++++
.../playlist_generate/.gh-pages/index.country.m3u | 14 ++++++++++++++
.../playlist_generate/logs/generators.log | 3 ++-
3 files changed, 31 insertions(+), 1 deletion(-)
create mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u
new file mode 100644
index 0000000000..1770572503
--- /dev/null
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u
@@ -0,0 +1,15 @@
+#EXTM3U
+#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
+#EXTVLCOPT:http-referrer=http://imn.iq
+#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
+#KODIPROP:inputstream=inputstream.adaptive
+#KODIPROP:inputstream.adaptive.manifest_type=mpd
+#KODIPROP:inputstream.adaptive.license_type=com.widevine.alpha
+#KODIPROP:inputstream.adaptive.license_key=https://drm.ors.at/acquire-license/widevine?BrandGuid=13f2e056-53fe-4469-ba6d-999970dbe549&userToken=v9ZVSksv4S7rT55o10dmYNRa4asye3z05eWCFxD%2FFYIlTJEpuf6tF8asPcyQOFq0h5opS%2B6WoMxnshWkihpHq5qrdrBEZ69piE94J9Feh385snGOqK3PYO7tLLjxmsCAe%2B9%2BNnurSSO5RCAIRsL125nSj1eOR%2F1GSKOgGH80HK2FDLiePxPkeaAxuWzacNBB%2FqnIGGxfe3GlmN65cU9F8WEpKFDlaxW%2Fv3ZSLAp3%2BZEq1aZXJ6Oz%2Fi0diD0EybH7|Content-Type=application/octet-stream|R{SSM}|
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8|Referer="https://referer.xyz/"|User-Agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1"|Origin="https://origin.xyz"
+#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="https://i.imgur.com/CnhTn8i.png" group-title="Undefined",ATV HD
+https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
+#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
+http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
+#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="https://i.imgur.com/ciTJrnl.png" group-title="Undefined",Zoo (720p)
+https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
index 063a191f7e..ee3dd3c29c 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
@@ -13,3 +13,17 @@ http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Uzbekistan",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
+#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
+#EXTVLCOPT:http-referrer=http://imn.iq
+#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
+#KODIPROP:inputstream=inputstream.adaptive
+#KODIPROP:inputstream.adaptive.manifest_type=mpd
+#KODIPROP:inputstream.adaptive.license_type=com.widevine.alpha
+#KODIPROP:inputstream.adaptive.license_key=https://drm.ors.at/acquire-license/widevine?BrandGuid=13f2e056-53fe-4469-ba6d-999970dbe549&userToken=v9ZVSksv4S7rT55o10dmYNRa4asye3z05eWCFxD%2FFYIlTJEpuf6tF8asPcyQOFq0h5opS%2B6WoMxnshWkihpHq5qrdrBEZ69piE94J9Feh385snGOqK3PYO7tLLjxmsCAe%2B9%2BNnurSSO5RCAIRsL125nSj1eOR%2F1GSKOgGH80HK2FDLiePxPkeaAxuWzacNBB%2FqnIGGxfe3GlmN65cU9F8WEpKFDlaxW%2Fv3ZSLAp3%2BZEq1aZXJ6Oz%2Fi0diD0EybH7|Content-Type=application/octet-stream|R{SSM}|
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8|Referer="https://referer.xyz/"|User-Agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1"|Origin="https://origin.xyz"
+#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="https://i.imgur.com/CnhTn8i.png" group-title="Undefined",ATV HD
+https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
+#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
+http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
+#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="https://i.imgur.com/ciTJrnl.png" group-title="Undefined",Zoo (720p)
+https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
diff --git a/tests/__data__/expected/playlist_generate/logs/generators.log b/tests/__data__/expected/playlist_generate/logs/generators.log
index 557bf67dbb..763d510883 100644
--- a/tests/__data__/expected/playlist_generate/logs/generators.log
+++ b/tests/__data__/expected/playlist_generate/logs/generators.log
@@ -44,6 +44,7 @@
{"type":"country","filepath":"countries/tm.m3u","count":1}
{"type":"country","filepath":"countries/kg.m3u","count":1}
{"type":"country","filepath":"countries/uz.m3u","count":1}
+{"type":"country","filepath":"countries/undefined.m3u","count":4}
{"type":"subdivision","filepath":"subdivisions/ad-02.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-ab.m3u","count":1}
{"type":"region","filepath":"regions/afr.m3u","count":0}
@@ -175,6 +176,6 @@
{"type":"source","filepath":"sources/kg.m3u","count":1}
{"type":"index","filepath":"index.m3u","count":11}
{"type":"index","filepath":"index.category.m3u","count":12}
-{"type":"index","filepath":"index.country.m3u","count":7}
+{"type":"index","filepath":"index.country.m3u","count":11}
{"type":"index","filepath":"index.language.m3u","count":11}
{"type":"index","filepath":"index.region.m3u","count":12}
From 25f2cd8a3250844afc613ed51ee983ce93809a67 Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sat, 23 Aug 2025 19:48:58 +0300
Subject: [PATCH 08/15] Update scripts
---
scripts/generators/countriesGenerator.ts | 12 ++++++++++++
scripts/generators/indexCountryGenerator.ts | 13 +++++++++++--
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/scripts/generators/countriesGenerator.ts b/scripts/generators/countriesGenerator.ts
index 2cd5cab760..4cd539deaf 100644
--- a/scripts/generators/countriesGenerator.ts
+++ b/scripts/generators/countriesGenerator.ts
@@ -40,5 +40,17 @@ export class CountriesGenerator implements Generator {
JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() }) + EOL
)
})
+
+ const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea())
+ const undefinedPlaylist = new Playlist(undefinedStreams, { public: true })
+ const undefinedFilepath = 'countries/undefined.m3u'
+ await this.storage.save(undefinedFilepath, undefinedPlaylist.toString())
+ this.logFile.append(
+ JSON.stringify({
+ type: 'country',
+ filepath: undefinedFilepath,
+ count: undefinedPlaylist.streams.count()
+ }) + EOL
+ )
}
}
diff --git a/scripts/generators/indexCountryGenerator.ts b/scripts/generators/indexCountryGenerator.ts
index c7e5e2f848..7384dd8836 100644
--- a/scripts/generators/indexCountryGenerator.ts
+++ b/scripts/generators/indexCountryGenerator.ts
@@ -26,7 +26,12 @@ export class IndexCountryGenerator implements Generator {
.orderBy((stream: Stream) => stream.getTitle())
.filter((stream: Stream) => stream.isSFW())
.forEach((stream: Stream) => {
- if (!stream.hasBroadcastArea()) return
+ if (!stream.hasBroadcastArea()) {
+ const streamClone = stream.clone()
+ streamClone.groupTitle = 'Undefined'
+ groupedStreams.add(streamClone)
+ return
+ }
stream.getBroadcastCountries().forEach((country: Country) => {
const streamClone = stream.clone()
@@ -35,7 +40,11 @@ export class IndexCountryGenerator implements Generator {
})
})
- groupedStreams = groupedStreams.orderBy((stream: Stream) => stream.groupTitle)
+ groupedStreams = groupedStreams.orderBy((stream: Stream) => {
+ if (stream.groupTitle === 'Undefined') return 'ZZZ'
+
+ return stream.groupTitle
+ })
const playlist = new Playlist(groupedStreams, { public: true })
const filepath = 'index.country.m3u'
From 676f6757919f01c9c9063cd49a4104e9233c96ff Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sun, 24 Aug 2025 13:04:57 +0300
Subject: [PATCH 09/15] Update tests/__data__
---
.../.gh-pages/index.country.m3u | 10 -
.../.gh-pages/index.region.m3u | 14 --
.../.gh-pages/regions/amer.m3u | 2 -
.../.gh-pages/regions/asia.m3u | 2 -
.../.gh-pages/regions/cis.m3u | 2 -
.../.gh-pages/regions/emea.m3u | 2 -
.../.gh-pages/regions/eur.m3u | 2 -
.../.gh-pages/regions/nam.m3u | 2 -
.../.gh-pages/regions/noram.m3u | 2 -
.../.gh-pages/subdivisions/ca-on.m3u | 2 -
.../playlist_generate/logs/generators.log | 176 ++++--------------
11 files changed, 38 insertions(+), 178 deletions(-)
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
index ee3dd3c29c..d56277635e 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
@@ -1,18 +1,8 @@
#EXTM3U
#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Canada",5AAB TV
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Kazakhstan",ЭлТР (480p) [Not 24/7]
-http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Kyrgyzstan",ЭлТР (480p) [Not 24/7]
-http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Russia",ЛДПР ТВ (1080p)
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
-#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Tajikistan",ЭлТР (480p) [Not 24/7]
-http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Turkmenistan",ЭлТР (480p) [Not 24/7]
-http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Uzbekistan",ЭлТР (480p) [Not 24/7]
-http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
#EXTVLCOPT:http-referrer=http://imn.iq
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u
index 0488050b70..d4ab4a8fda 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u
@@ -1,25 +1,11 @@
#EXTM3U
-#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Americas",5AAB TV
-http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Asia",ЛДПР ТВ (1080p)
-http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Asia",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Central Asia",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Commonwealth of Independent States",ЛДПР ТВ (1080p)
-http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Commonwealth of Independent States",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe",ЛДПР ТВ (1080p)
-http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe, the Middle East and Africa",ЛДПР ТВ (1080p)
-http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe, the Middle East and Africa",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
-#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="North America",5AAB TV
-http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
-#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Northern America",5AAB TV
-http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u
index 754a6969c0..fcd718794a 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u
@@ -1,3 +1 @@
#EXTM3U
-#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
-http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u
index 6f6d448e87..a9387b8b44 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u
@@ -1,5 +1,3 @@
#EXTM3U
-#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
-http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u
index 6f6d448e87..a9387b8b44 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u
@@ -1,5 +1,3 @@
#EXTM3U
-#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
-http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u
index 6f6d448e87..a9387b8b44 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u
@@ -1,5 +1,3 @@
#EXTM3U
-#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
-http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u
index 6f6d448e87..a9387b8b44 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u
@@ -1,5 +1,3 @@
#EXTM3U
-#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
-http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u
index 754a6969c0..fcd718794a 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u
@@ -1,3 +1 @@
#EXTM3U
-#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
-http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u
index 754a6969c0..fcd718794a 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u
@@ -1,3 +1 @@
#EXTM3U
-#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
-http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u
index 8aeaaae90e..4686c68f4b 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u
@@ -1,5 +1,3 @@
#EXTM3U
-#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
-http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/logs/generators.log b/tests/__data__/expected/playlist_generate/logs/generators.log
index 763d510883..d3b3a67bdf 100644
--- a/tests/__data__/expected/playlist_generate/logs/generators.log
+++ b/tests/__data__/expected/playlist_generate/logs/generators.log
@@ -5,170 +5,70 @@
{"type":"raw","filepath":"raw/uk.m3u","count":1}
{"type":"raw","filepath":"raw/unsorted.m3u","count":4}
{"type":"category","filepath":"categories/auto.m3u","count":0}
-{"type":"category","filepath":"categories/animation.m3u","count":0}
{"type":"category","filepath":"categories/business.m3u","count":0}
+{"type":"category","filepath":"categories/animation.m3u","count":0}
{"type":"category","filepath":"categories/classic.m3u","count":0}
-{"type":"category","filepath":"categories/cooking.m3u","count":0}
{"type":"category","filepath":"categories/comedy.m3u","count":0}
-{"type":"category","filepath":"categories/education.m3u","count":0}
{"type":"category","filepath":"categories/documentary.m3u","count":0}
-{"type":"category","filepath":"categories/family.m3u","count":0}
-{"type":"category","filepath":"categories/culture.m3u","count":0}
{"type":"category","filepath":"categories/entertainment.m3u","count":0}
{"type":"category","filepath":"categories/general.m3u","count":3}
+{"type":"category","filepath":"categories/education.m3u","count":0}
+{"type":"category","filepath":"categories/family.m3u","count":0}
+{"type":"category","filepath":"categories/movies.m3u","count":0}
+{"type":"category","filepath":"categories/kids.m3u","count":0}
+{"type":"category","filepath":"categories/cooking.m3u","count":0}
{"type":"category","filepath":"categories/lifestyle.m3u","count":0}
{"type":"category","filepath":"categories/legislative.m3u","count":0}
-{"type":"category","filepath":"categories/movies.m3u","count":0}
-{"type":"category","filepath":"categories/news.m3u","count":1}
-{"type":"category","filepath":"categories/kids.m3u","count":0}
-{"type":"category","filepath":"categories/relax.m3u","count":0}
-{"type":"category","filepath":"categories/music.m3u","count":0}
-{"type":"category","filepath":"categories/science.m3u","count":0}
{"type":"category","filepath":"categories/outdoor.m3u","count":0}
+{"type":"category","filepath":"categories/news.m3u","count":1}
+{"type":"category","filepath":"categories/culture.m3u","count":0}
+{"type":"category","filepath":"categories/music.m3u","count":0}
{"type":"category","filepath":"categories/series.m3u","count":0}
-{"type":"category","filepath":"categories/shop.m3u","count":0}
-{"type":"category","filepath":"categories/travel.m3u","count":0}
-{"type":"category","filepath":"categories/xxx.m3u","count":1}
-{"type":"category","filepath":"categories/weather.m3u","count":1}
-{"type":"category","filepath":"categories/undefined.m3u","count":7}
{"type":"category","filepath":"categories/sports.m3u","count":0}
{"type":"category","filepath":"categories/religious.m3u","count":0}
+{"type":"category","filepath":"categories/relax.m3u","count":0}
+{"type":"category","filepath":"categories/weather.m3u","count":1}
+{"type":"category","filepath":"categories/shop.m3u","count":0}
+{"type":"category","filepath":"categories/travel.m3u","count":0}
+{"type":"category","filepath":"categories/science.m3u","count":0}
+{"type":"category","filepath":"categories/undefined.m3u","count":7}
+{"type":"category","filepath":"categories/xxx.m3u","count":1}
{"type":"language","filepath":"languages/cat.m3u","count":1}
+{"type":"language","filepath":"languages/eng.m3u","count":1}
{"type":"language","filepath":"languages/rus.m3u","count":1}
{"type":"language","filepath":"languages/undefined.m3u","count":8}
-{"type":"language","filepath":"languages/eng.m3u","count":1}
-{"type":"country","filepath":"countries/ca.m3u","count":1}
-{"type":"country","filepath":"countries/kz.m3u","count":1}
-{"type":"country","filepath":"countries/ru.m3u","count":1}
-{"type":"country","filepath":"countries/tj.m3u","count":1}
-{"type":"country","filepath":"countries/tm.m3u","count":1}
-{"type":"country","filepath":"countries/kg.m3u","count":1}
-{"type":"country","filepath":"countries/uz.m3u","count":1}
{"type":"country","filepath":"countries/undefined.m3u","count":4}
+{"type":"country","filepath":"countries/ca.m3u","count":1}
+{"type":"country","filepath":"countries/ru.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ad-02.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-ab.m3u","count":1}
{"type":"region","filepath":"regions/afr.m3u","count":0}
-{"type":"subdivision","filepath":"subdivisions/ca-nb.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-mb.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-bc.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-nt.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-nl.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":2}
-{"type":"subdivision","filepath":"subdivisions/ca-qc.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-ns.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-nu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ad.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-yt.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-pe.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ca-sk.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ba.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-al.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kr.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-bu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kk.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kl.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-da.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ko.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-me.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-in.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-alt.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-se.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ty.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ta.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-mo.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ark.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-bel.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-sa.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-amu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ast.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ce.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-bry.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-che.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-cu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-irk.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-chu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kgd.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kb.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-klu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kam.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kc.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-khm.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kha.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kem.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-iva.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kda.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kya.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kir.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kgn.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-mos.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-kos.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-krs.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-mag.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-len.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-mur.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-lip.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ngr.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-nen.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-mow.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-niz.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-orl.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-pnz.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-nvs.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-oms.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-per.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ore.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-pri.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ros.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-sak.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-sta.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-sam.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-rya.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-smo.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-psk.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-spe.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-sve.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-tam.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-ud.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-tom.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-sar.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-tyu.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-tve.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-uly.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-vor.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-vgg.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-tul.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-vlg.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-vla.m3u","count":1}
-{"type":"region","filepath":"regions/apac.m3u","count":0}
-{"type":"subdivision","filepath":"subdivisions/ru-yan.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-yar.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ru-yev.m3u","count":1}
+{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":1}
+{"type":"source","filepath":"sources/in.m3u","count":1}
+{"type":"region","filepath":"regions/asia.m3u","count":1}
{"type":"region","filepath":"regions/arab.m3u","count":0}
-{"type":"region","filepath":"regions/asean.m3u","count":0}
-{"type":"subdivision","filepath":"subdivisions/ru-zab.m3u","count":1}
-{"type":"region","filepath":"regions/amer.m3u","count":1}
-{"type":"region","filepath":"regions/asia.m3u","count":2}
-{"type":"region","filepath":"regions/cis.m3u","count":2}
-{"type":"region","filepath":"regions/cas.m3u","count":1}
+{"type":"region","filepath":"regions/amer.m3u","count":0}
{"type":"region","filepath":"regions/carib.m3u","count":0}
-{"type":"region","filepath":"regions/emea.m3u","count":2}
-{"type":"region","filepath":"regions/lac.m3u","count":0}
-{"type":"region","filepath":"regions/hispam.m3u","count":0}
+{"type":"region","filepath":"regions/asean.m3u","count":0}
+{"type":"region","filepath":"regions/apac.m3u","count":0}
+{"type":"region","filepath":"regions/cas.m3u","count":1}
+{"type":"region","filepath":"regions/emea.m3u","count":1}
+{"type":"region","filepath":"regions/eur.m3u","count":1}
+{"type":"region","filepath":"regions/cis.m3u","count":1}
{"type":"region","filepath":"regions/cenamer.m3u","count":0}
{"type":"region","filepath":"regions/latam.m3u","count":0}
-{"type":"region","filepath":"regions/nam.m3u","count":1}
+{"type":"region","filepath":"regions/hispam.m3u","count":0}
+{"type":"region","filepath":"regions/lac.m3u","count":0}
{"type":"region","filepath":"regions/maghreb.m3u","count":0}
-{"type":"region","filepath":"regions/eur.m3u","count":2}
-{"type":"region","filepath":"regions/mena.m3u","count":0}
+{"type":"region","filepath":"regions/nam.m3u","count":0}
{"type":"region","filepath":"regions/mideast.m3u","count":0}
+{"type":"region","filepath":"regions/noram.m3u","count":0}
+{"type":"region","filepath":"regions/mena.m3u","count":0}
+{"type":"region","filepath":"regions/oce.m3u","count":0}
{"type":"region","filepath":"regions/nord.m3u","count":0}
{"type":"region","filepath":"regions/sas.m3u","count":0}
-{"type":"region","filepath":"regions/noram.m3u","count":1}
-{"type":"region","filepath":"regions/oce.m3u","count":0}
-{"type":"region","filepath":"regions/wafr.m3u","count":0}
{"type":"region","filepath":"regions/ssa.m3u","count":0}
{"type":"region","filepath":"regions/southam.m3u","count":0}
-{"type":"source","filepath":"sources/in.m3u","count":1}
+{"type":"region","filepath":"regions/wafr.m3u","count":0}
{"type":"source","filepath":"sources/unsorted.m3u","count":4}
{"type":"source","filepath":"sources/ca.m3u","count":2}
{"type":"source","filepath":"sources/ad.m3u","count":3}
@@ -176,6 +76,6 @@
{"type":"source","filepath":"sources/kg.m3u","count":1}
{"type":"index","filepath":"index.m3u","count":11}
{"type":"index","filepath":"index.category.m3u","count":12}
-{"type":"index","filepath":"index.country.m3u","count":11}
+{"type":"index","filepath":"index.country.m3u","count":6}
{"type":"index","filepath":"index.language.m3u","count":11}
-{"type":"index","filepath":"index.region.m3u","count":12}
+{"type":"index","filepath":"index.region.m3u","count":5}
From 5b50df99a386268e30dc52ee2d35a6803fe1654b Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sun, 24 Aug 2025 13:05:03 +0300
Subject: [PATCH 10/15] Update broadcastArea.ts
---
scripts/models/broadcastArea.ts | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/scripts/models/broadcastArea.ts b/scripts/models/broadcastArea.ts
index cd74efcefd..be759de723 100644
--- a/scripts/models/broadcastArea.ts
+++ b/scripts/models/broadcastArea.ts
@@ -38,19 +38,16 @@ export class BroadcastArea {
if (!subdivision) return
citiesIncluded = citiesIncluded.concat(subdivision.getCities())
subdivisionsIncluded.add(subdivision)
+ if (subdivision.country) subdivisionsIncluded.add(subdivision.country)
}
case 'c': {
const country: Country = countriesKeyByCode.get(code)
if (!country) return
- citiesIncluded = citiesIncluded.concat(country.getCities())
- subdivisionsIncluded = subdivisionsIncluded.concat(country.getSubdivisions())
countriesIncluded.add(country)
- regionsIncluded = regionsIncluded.concat(country.getRegions())
}
case 'r': {
const region: Region = regionsKeyByCode.get(code)
if (!region) return
- countriesIncluded = countriesIncluded.concat(region.getCountries())
regionsIncluded = regionsIncluded.concat(region.getRegions())
}
}
From 882fac2b33668681f68ff896cc1f5b0d8e9112bd Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sun, 24 Aug 2025 13:14:45 +0300
Subject: [PATCH 11/15] Update broadcastArea.ts
---
scripts/models/broadcastArea.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/scripts/models/broadcastArea.ts b/scripts/models/broadcastArea.ts
index be759de723..c7392984b1 100644
--- a/scripts/models/broadcastArea.ts
+++ b/scripts/models/broadcastArea.ts
@@ -38,7 +38,6 @@ export class BroadcastArea {
if (!subdivision) return
citiesIncluded = citiesIncluded.concat(subdivision.getCities())
subdivisionsIncluded.add(subdivision)
- if (subdivision.country) subdivisionsIncluded.add(subdivision.country)
}
case 'c': {
const country: Country = countriesKeyByCode.get(code)
From 578b1352402050a981f6c8d3e356c9f949a33eda Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sun, 24 Aug 2025 22:42:55 +0300
Subject: [PATCH 12/15] Update tests/__data__
---
.../ad-02.m3u => cities/adcan.m3u} | 0
.../.gh-pages/countries/int.m3u | 7 +
.../.gh-pages/index.country.m3u | 6 +
.../.gh-pages/index.region.m3u | 128 +++++++++++++++
.../.gh-pages/regions/afr.m3u | 4 +
.../.gh-pages/regions/amer.m3u | 8 +
.../.gh-pages/regions/apac.m3u | 4 +
.../.gh-pages/regions/arab.m3u | 4 +
.../.gh-pages/regions/asean.m3u | 5 +
.../.gh-pages/regions/asia.m3u | 6 +
.../.gh-pages/regions/carib.m3u | 4 +
.../.gh-pages/regions/cas.m3u | 4 +
.../.gh-pages/regions/cenamer.m3u | 5 +
.../.gh-pages/regions/cis.m3u | 6 +
.../.gh-pages/regions/emea.m3u | 8 +
.../.gh-pages/regions/eur.m3u | 8 +
.../.gh-pages/regions/hispam.m3u | 4 +
.../.gh-pages/regions/lac.m3u | 4 +
.../.gh-pages/regions/latam.m3u | 4 +
.../.gh-pages/regions/maghreb.m3u | 4 +
.../.gh-pages/regions/mena.m3u | 4 +
.../.gh-pages/regions/mideast.m3u | 4 +
.../.gh-pages/regions/nam.m3u | 8 +
.../.gh-pages/regions/noram.m3u | 8 +
.../.gh-pages/regions/nord.m3u | 4 +
.../.gh-pages/regions/oce.m3u | 4 +
.../.gh-pages/regions/sas.m3u | 4 +
.../.gh-pages/regions/southam.m3u | 5 +
.../.gh-pages/regions/ssa.m3u | 4 +
.../.gh-pages/regions/wafr.m3u | 4 +
.../playlist_generate/logs/generators.log | 97 +++++------
.../expected/readme_update/playlists.md | 152 +++++++-----------
tests/__data__/input/data/cities.json | 2 +-
tests/__data__/input/data/feeds.json | 6 +-
tests/__data__/input/data/regions.json | 2 +-
.../input/readme_update/.readme/config.json | 4 -
.../input/readme_update/.readme/template.md | 110 -------------
.../input/readme_update/generators.log | 3 +
38 files changed, 388 insertions(+), 260 deletions(-)
rename tests/__data__/expected/playlist_generate/.gh-pages/{subdivisions/ad-02.m3u => cities/adcan.m3u} (100%)
create mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/countries/int.m3u
create mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/regions/asean.m3u
create mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/regions/cenamer.m3u
create mode 100644 tests/__data__/expected/playlist_generate/.gh-pages/regions/southam.m3u
delete mode 100644 tests/__data__/input/readme_update/.readme/config.json
delete mode 100644 tests/__data__/input/readme_update/.readme/template.md
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ad-02.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/cities/adcan.m3u
similarity index 100%
rename from tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ad-02.m3u
rename to tests/__data__/expected/playlist_generate/.gh-pages/cities/adcan.m3u
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/countries/int.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/countries/int.m3u
new file mode 100644
index 0000000000..4e507cf645
--- /dev/null
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/countries/int.m3u
@@ -0,0 +1,7 @@
+#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
+http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
index d56277635e..8cc0cf2622 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u
@@ -3,6 +3,12 @@
http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Russia",ЛДПР ТВ (1080p)
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="International",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="International",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="International",ЭлТР (480p) [Not 24/7]
+http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
#EXTVLCOPT:http-referrer=http://imn.iq
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u
index d4ab4a8fda..d8d2d7f6e8 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u
@@ -1,11 +1,139 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Africa",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Africa",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Americas",5AAB TV
+http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Americas",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Americas",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Americas",Meteomedia
+http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Arab world",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Arab world",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Asia",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Asia",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Asia",ЛДПР ТВ (1080p)
+http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Asia",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Asia-Pacific",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Asia-Pacific",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Association of Southeast Asian Nations",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Association of Southeast Asian Nations",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Caribbean",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Caribbean",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Central America",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Central America",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Central Asia",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Central Asia",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Central Asia",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Commonwealth of Independent States",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Commonwealth of Independent States",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Commonwealth of Independent States",ЛДПР ТВ (1080p)
+http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Commonwealth of Independent States",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
+#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Europe",ATV
+https://iptv-all.lanesh4d0w.repl.co/andorra/atv
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Europe",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Europe",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe",ЛДПР ТВ (1080p)
+http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
+#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Europe, the Middle East and Africa",ATV
+https://iptv-all.lanesh4d0w.repl.co/andorra/atv
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Europe, the Middle East and Africa",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Europe, the Middle East and Africa",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe, the Middle East and Africa",ЛДПР ТВ (1080p)
+http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe, the Middle East and Africa",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Hispanic America",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Hispanic America",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Latin America",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Latin America",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Latin America and the Caribbean",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Latin America and the Caribbean",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Maghreb",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Maghreb",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Middle East",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Middle East",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Middle East and North Africa",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Middle East and North Africa",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Nordics",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Nordics",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="North America",5AAB TV
+http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="North America",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="North America",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="North America",Meteomedia
+http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
+#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Northern America",5AAB TV
+http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Northern America",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Northern America",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Northern America",Meteomedia
+http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Oceania",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Oceania",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="South America",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="South America",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="South Asia",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="South Asia",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="Sub-Saharan Africa",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Sub-Saharan Africa",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="West Africa",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="West Africa",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/afr.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/afr.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/afr.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/afr.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u
index fcd718794a..2151d32ae8 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u
@@ -1 +1,9 @@
#EXTM3U
+#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
+http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
+http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/apac.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/apac.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/apac.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/apac.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/arab.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/arab.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/arab.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/arab.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/asean.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/asean.m3u
new file mode 100644
index 0000000000..c549c09ce1
--- /dev/null
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/asean.m3u
@@ -0,0 +1,5 @@
+#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u
index a9387b8b44..202cd4e3fa 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u
@@ -1,3 +1,9 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
+http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/carib.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/carib.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/carib.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/carib.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/cas.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cas.m3u
index a9387b8b44..4e507cf645 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/cas.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cas.m3u
@@ -1,3 +1,7 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/cenamer.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cenamer.m3u
new file mode 100644
index 0000000000..c549c09ce1
--- /dev/null
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cenamer.m3u
@@ -0,0 +1,5 @@
+#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u
index a9387b8b44..202cd4e3fa 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u
@@ -1,3 +1,9 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
+http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u
index a9387b8b44..9a8e344397 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u
@@ -1,3 +1,11 @@
#EXTM3U
+#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Undefined",ATV
+https://iptv-all.lanesh4d0w.repl.co/andorra/atv
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
+http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u
index a9387b8b44..9a8e344397 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u
@@ -1,3 +1,11 @@
#EXTM3U
+#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="https://i.imgur.com/BnhTn8i.png" group-title="Undefined",ATV
+https://iptv-all.lanesh4d0w.repl.co/andorra/atv
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
+http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/hispam.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/hispam.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/hispam.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/hispam.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/lac.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/lac.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/lac.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/lac.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/latam.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/latam.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/latam.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/latam.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/maghreb.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/maghreb.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/maghreb.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/maghreb.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/mena.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/mena.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/mena.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/mena.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/mideast.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/mideast.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/mideast.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/mideast.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u
index fcd718794a..2151d32ae8 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u
@@ -1 +1,9 @@
#EXTM3U
+#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
+http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
+http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u
index fcd718794a..2151d32ae8 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u
@@ -1 +1,9 @@
#EXTM3U
+#EXTINF:-1 tvg-id="5AABTV.ca" tvg-logo="" group-title="Undefined",5AAB TV
+http://158.69.124.9:1935/5aabtv/5aabtv/playlist.m3u8
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
+#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
+http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nord.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nord.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nord.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nord.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/oce.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/oce.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/oce.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/oce.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/sas.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/sas.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/sas.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/sas.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/southam.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/southam.m3u
new file mode 100644
index 0000000000..c549c09ce1
--- /dev/null
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/southam.m3u
@@ -0,0 +1,5 @@
+#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/ssa.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/ssa.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/ssa.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/ssa.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/wafr.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/wafr.m3u
index fcd718794a..c549c09ce1 100644
--- a/tests/__data__/expected/playlist_generate/.gh-pages/regions/wafr.m3u
+++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/wafr.m3u
@@ -1 +1,5 @@
#EXTM3U
+#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
+http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
+#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
+http://146.59.85.40:89/dunaworld/index.m3u8
diff --git a/tests/__data__/expected/playlist_generate/logs/generators.log b/tests/__data__/expected/playlist_generate/logs/generators.log
index d3b3a67bdf..6dc3d2af68 100644
--- a/tests/__data__/expected/playlist_generate/logs/generators.log
+++ b/tests/__data__/expected/playlist_generate/logs/generators.log
@@ -5,70 +5,71 @@
{"type":"raw","filepath":"raw/uk.m3u","count":1}
{"type":"raw","filepath":"raw/unsorted.m3u","count":4}
{"type":"category","filepath":"categories/auto.m3u","count":0}
-{"type":"category","filepath":"categories/business.m3u","count":0}
{"type":"category","filepath":"categories/animation.m3u","count":0}
+{"type":"category","filepath":"categories/business.m3u","count":0}
{"type":"category","filepath":"categories/classic.m3u","count":0}
-{"type":"category","filepath":"categories/comedy.m3u","count":0}
-{"type":"category","filepath":"categories/documentary.m3u","count":0}
-{"type":"category","filepath":"categories/entertainment.m3u","count":0}
-{"type":"category","filepath":"categories/general.m3u","count":3}
-{"type":"category","filepath":"categories/education.m3u","count":0}
-{"type":"category","filepath":"categories/family.m3u","count":0}
-{"type":"category","filepath":"categories/movies.m3u","count":0}
-{"type":"category","filepath":"categories/kids.m3u","count":0}
{"type":"category","filepath":"categories/cooking.m3u","count":0}
-{"type":"category","filepath":"categories/lifestyle.m3u","count":0}
-{"type":"category","filepath":"categories/legislative.m3u","count":0}
-{"type":"category","filepath":"categories/outdoor.m3u","count":0}
-{"type":"category","filepath":"categories/news.m3u","count":1}
{"type":"category","filepath":"categories/culture.m3u","count":0}
+{"type":"category","filepath":"categories/education.m3u","count":0}
+{"type":"category","filepath":"categories/documentary.m3u","count":0}
+{"type":"category","filepath":"categories/comedy.m3u","count":0}
+{"type":"category","filepath":"categories/family.m3u","count":0}
+{"type":"category","filepath":"categories/kids.m3u","count":0}
+{"type":"category","filepath":"categories/legislative.m3u","count":0}
+{"type":"category","filepath":"categories/entertainment.m3u","count":0}
{"type":"category","filepath":"categories/music.m3u","count":0}
+{"type":"category","filepath":"categories/outdoor.m3u","count":0}
+{"type":"category","filepath":"categories/general.m3u","count":3}
+{"type":"category","filepath":"categories/lifestyle.m3u","count":0}
+{"type":"category","filepath":"categories/relax.m3u","count":0}
+{"type":"category","filepath":"categories/religious.m3u","count":0}
+{"type":"category","filepath":"categories/movies.m3u","count":0}
+{"type":"category","filepath":"categories/shop.m3u","count":0}
+{"type":"category","filepath":"categories/science.m3u","count":0}
+{"type":"category","filepath":"categories/news.m3u","count":1}
{"type":"category","filepath":"categories/series.m3u","count":0}
{"type":"category","filepath":"categories/sports.m3u","count":0}
-{"type":"category","filepath":"categories/religious.m3u","count":0}
-{"type":"category","filepath":"categories/relax.m3u","count":0}
{"type":"category","filepath":"categories/weather.m3u","count":1}
-{"type":"category","filepath":"categories/shop.m3u","count":0}
{"type":"category","filepath":"categories/travel.m3u","count":0}
-{"type":"category","filepath":"categories/science.m3u","count":0}
-{"type":"category","filepath":"categories/undefined.m3u","count":7}
{"type":"category","filepath":"categories/xxx.m3u","count":1}
+{"type":"category","filepath":"categories/undefined.m3u","count":7}
{"type":"language","filepath":"languages/cat.m3u","count":1}
{"type":"language","filepath":"languages/eng.m3u","count":1}
{"type":"language","filepath":"languages/rus.m3u","count":1}
{"type":"language","filepath":"languages/undefined.m3u","count":8}
-{"type":"country","filepath":"countries/undefined.m3u","count":4}
{"type":"country","filepath":"countries/ca.m3u","count":1}
+{"type":"country","filepath":"countries/int.m3u","count":3}
{"type":"country","filepath":"countries/ru.m3u","count":1}
-{"type":"subdivision","filepath":"subdivisions/ad-02.m3u","count":1}
-{"type":"region","filepath":"regions/afr.m3u","count":0}
+{"type":"country","filepath":"countries/undefined.m3u","count":4}
{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":1}
+{"type":"city","filepath":"cities/adcan.m3u","count":1}
+{"type":"region","filepath":"regions/afr.m3u","count":2}
+{"type":"region","filepath":"regions/amer.m3u","count":4}
+{"type":"region","filepath":"regions/apac.m3u","count":2}
+{"type":"region","filepath":"regions/carib.m3u","count":2}
{"type":"source","filepath":"sources/in.m3u","count":1}
-{"type":"region","filepath":"regions/asia.m3u","count":1}
-{"type":"region","filepath":"regions/arab.m3u","count":0}
-{"type":"region","filepath":"regions/amer.m3u","count":0}
-{"type":"region","filepath":"regions/carib.m3u","count":0}
-{"type":"region","filepath":"regions/asean.m3u","count":0}
-{"type":"region","filepath":"regions/apac.m3u","count":0}
-{"type":"region","filepath":"regions/cas.m3u","count":1}
-{"type":"region","filepath":"regions/emea.m3u","count":1}
-{"type":"region","filepath":"regions/eur.m3u","count":1}
-{"type":"region","filepath":"regions/cis.m3u","count":1}
-{"type":"region","filepath":"regions/cenamer.m3u","count":0}
-{"type":"region","filepath":"regions/latam.m3u","count":0}
-{"type":"region","filepath":"regions/hispam.m3u","count":0}
-{"type":"region","filepath":"regions/lac.m3u","count":0}
-{"type":"region","filepath":"regions/maghreb.m3u","count":0}
-{"type":"region","filepath":"regions/nam.m3u","count":0}
-{"type":"region","filepath":"regions/mideast.m3u","count":0}
-{"type":"region","filepath":"regions/noram.m3u","count":0}
-{"type":"region","filepath":"regions/mena.m3u","count":0}
-{"type":"region","filepath":"regions/oce.m3u","count":0}
-{"type":"region","filepath":"regions/nord.m3u","count":0}
-{"type":"region","filepath":"regions/sas.m3u","count":0}
-{"type":"region","filepath":"regions/ssa.m3u","count":0}
-{"type":"region","filepath":"regions/southam.m3u","count":0}
-{"type":"region","filepath":"regions/wafr.m3u","count":0}
+{"type":"region","filepath":"regions/asean.m3u","count":2}
+{"type":"region","filepath":"regions/arab.m3u","count":2}
+{"type":"region","filepath":"regions/asia.m3u","count":4}
+{"type":"region","filepath":"regions/emea.m3u","count":5}
+{"type":"region","filepath":"regions/cenamer.m3u","count":2}
+{"type":"region","filepath":"regions/cis.m3u","count":4}
+{"type":"region","filepath":"regions/cas.m3u","count":3}
+{"type":"region","filepath":"regions/hispam.m3u","count":2}
+{"type":"region","filepath":"regions/maghreb.m3u","count":2}
+{"type":"region","filepath":"regions/eur.m3u","count":5}
+{"type":"region","filepath":"regions/lac.m3u","count":2}
+{"type":"region","filepath":"regions/mideast.m3u","count":2}
+{"type":"region","filepath":"regions/noram.m3u","count":4}
+{"type":"region","filepath":"regions/latam.m3u","count":2}
+{"type":"region","filepath":"regions/oce.m3u","count":2}
+{"type":"region","filepath":"regions/nam.m3u","count":4}
+{"type":"region","filepath":"regions/southam.m3u","count":2}
+{"type":"region","filepath":"regions/mena.m3u","count":2}
+{"type":"region","filepath":"regions/wafr.m3u","count":2}
+{"type":"region","filepath":"regions/nord.m3u","count":2}
+{"type":"region","filepath":"regions/sas.m3u","count":2}
+{"type":"region","filepath":"regions/ssa.m3u","count":2}
{"type":"source","filepath":"sources/unsorted.m3u","count":4}
{"type":"source","filepath":"sources/ca.m3u","count":2}
{"type":"source","filepath":"sources/ad.m3u","count":3}
@@ -76,6 +77,6 @@
{"type":"source","filepath":"sources/kg.m3u","count":1}
{"type":"index","filepath":"index.m3u","count":11}
{"type":"index","filepath":"index.category.m3u","count":12}
-{"type":"index","filepath":"index.country.m3u","count":6}
+{"type":"index","filepath":"index.country.m3u","count":9}
{"type":"index","filepath":"index.language.m3u","count":11}
-{"type":"index","filepath":"index.region.m3u","count":5}
+{"type":"index","filepath":"index.region.m3u","count":69}
diff --git a/tests/__data__/expected/readme_update/playlists.md b/tests/__data__/expected/readme_update/playlists.md
index 9736435104..120e27f2af 100644
--- a/tests/__data__/expected/readme_update/playlists.md
+++ b/tests/__data__/expected/readme_update/playlists.md
@@ -1,13 +1,15 @@
-# Playlists
+## Playlists
+
+There are several versions of playlists that differ in the way they are grouped. As of January 30th, 2024, we have stopped distributing NSFW channels. For more information, please look at [this issue](https://github.com/iptv-org/iptv/issues/15723).
### Grouped by category
+Playlists in which channels are grouped by category.
+
Expand
-Playlist in which each channel has its _category_ as a group title:
-
```
https://iptv-org.github.io/iptv/index.category.m3u
```
@@ -56,12 +58,12 @@ Same thing, but split up into separate files:
### Grouped by language
+Playlists in which channels are grouped by the language in which they are broadcast.
+
Expand
-Playlist in which each channel has its _language_ as a group title:
-
```
https://iptv-org.github.io/iptv/index.language.m3u
```
@@ -84,113 +86,77 @@ Same thing, but split up into separate files:
-### Grouped by country
+### Grouped by broadcast area
+
+Playlists in which channels are grouped by broadcast area.
Expand
-
-Playlist in which each channel has its _country_ as a group title:
+#### Countries
```
-https://iptv-org.github.io/iptv/index.country.m3u
+https://iptv-org.github.io/iptv/index.countries.m3u
```
Same thing, but split up into separate files:
-
-
- | Country | Channels | Playlist |
-
-
- | 🇨🇲 Cameroon | 1 | https://iptv-org.github.io/iptv/countries/cm.m3u |
- | 🇨🇦 Canada | 2 | https://iptv-org.github.io/iptv/countries/ca.m3u |
- | 🇨🇻 Cape Verde | 1 | https://iptv-org.github.io/iptv/countries/cv.m3u |
- | 🇨🇬 Republic of the Congo | 1 | https://iptv-org.github.io/iptv/countries/cg.m3u |
- | 🇷🇪 Réunion | 1 | https://iptv-org.github.io/iptv/countries/re.m3u |
- | 🇷🇴 Romania | 1 | https://iptv-org.github.io/iptv/countries/ro.m3u |
- | 🇷🇺 Russia | 2 | https://iptv-org.github.io/iptv/countries/ru.m3u |
- | 🇷🇼 Rwanda | 1 | https://iptv-org.github.io/iptv/countries/rw.m3u |
- | 🇧🇱 Saint Barthélemy | 1 | https://iptv-org.github.io/iptv/countries/bl.m3u |
- | 🇸🇭 Saint Helena | 1 | https://iptv-org.github.io/iptv/countries/sh.m3u |
- | 🇰🇳 Saint Kitts and Nevis | 1 | https://iptv-org.github.io/iptv/countries/kn.m3u |
- | Undefined | 2 | https://iptv-org.github.io/iptv/countries/undefined.m3u |
-
-
+- 🇦🇩 Andorra https://iptv-org.github.io/iptv/countries/ad.m3u
+ - Canillo https://iptv-org.github.io/iptv/subdivisions/ad-02.m3u
+ - Canillo https://iptv-org.github.io/iptv/cities/adcan.m3u
+- 🇨🇲 Cameroon https://iptv-org.github.io/iptv/countries/cm.m3u
+- 🇨🇦 Canada https://iptv-org.github.io/iptv/countries/ca.m3u
+ - Ontario https://iptv-org.github.io/iptv/subdivisions/ca-on.m3u
+- 🇨🇻 Cape Verde https://iptv-org.github.io/iptv/countries/cv.m3u
+- 🇭🇰 Hong Kong https://iptv-org.github.io/iptv/countries/hk.m3u
+ - Sai Kung https://iptv-org.github.io/iptv/cities/hk9sk.m3u
+- 🇨🇬 Republic of the Congo https://iptv-org.github.io/iptv/countries/cg.m3u
+- 🇷🇪 Réunion https://iptv-org.github.io/iptv/countries/re.m3u
+- 🇷🇴 Romania https://iptv-org.github.io/iptv/countries/ro.m3u
+- 🇷🇺 Russia https://iptv-org.github.io/iptv/countries/ru.m3u
+- 🇷🇼 Rwanda https://iptv-org.github.io/iptv/countries/rw.m3u
+- 🇧🇱 Saint Barthélemy https://iptv-org.github.io/iptv/countries/bl.m3u
+- 🇸🇭 Saint Helena https://iptv-org.github.io/iptv/countries/sh.m3u
+- 🇰🇳 Saint Kitts and Nevis https://iptv-org.github.io/iptv/countries/kn.m3u
+- 🌐 International https://iptv-org.github.io/iptv/countries/int.m3u
+- Undefined https://iptv-org.github.io/iptv/countries/undefined.m3u
-
-
-### Grouped by subdivision
-
-
-Expand
-
-
-
-
-Canada
-
-
- | Subdivision | Channels | Playlist |
-
-
- | Ontario | 1 | https://iptv-org.github.io/iptv/subdivisions/ca-on.m3u |
-
-
-
-
-
-
-### Grouped by region
-
-
-Expand
-
-
-Playlist in which each channel has its _region_ as a group title:
+#### Regions
```
-https://iptv-org.github.io/iptv/index.region.m3u
+https://iptv-org.github.io/iptv/index.regions.m3u
```
Same thing, but split up into separate files:
-
-
- | Region | Channels | Playlist |
-
-
- | Africa | 0 | https://iptv-org.github.io/iptv/regions/afr.m3u |
- | Americas | 1 | https://iptv-org.github.io/iptv/regions/amer.m3u |
- | Arab world | 0 | https://iptv-org.github.io/iptv/regions/arab.m3u |
- | Asia | 2 | https://iptv-org.github.io/iptv/regions/asia.m3u |
- | Asia-Pacific | 1 | https://iptv-org.github.io/iptv/regions/apac.m3u |
- | Association of Southeast Asian Nations | 0 | https://iptv-org.github.io/iptv/regions/asean.m3u |
- | Caribbean | 0 | https://iptv-org.github.io/iptv/regions/carib.m3u |
- | Central America | 0 | https://iptv-org.github.io/iptv/regions/cenamer.m3u |
- | Central Asia | 0 | https://iptv-org.github.io/iptv/regions/cas.m3u |
- | Commonwealth of Independent States | 1 | https://iptv-org.github.io/iptv/regions/cis.m3u |
- | Europe | 3 | https://iptv-org.github.io/iptv/regions/eur.m3u |
- | Europe, the Middle East and Africa | 3 | https://iptv-org.github.io/iptv/regions/emea.m3u |
- | Hispanic America | 0 | https://iptv-org.github.io/iptv/regions/hispam.m3u |
- | Latin America | 0 | https://iptv-org.github.io/iptv/regions/latam.m3u |
- | Latin America and the Caribbean | 0 | https://iptv-org.github.io/iptv/regions/lac.m3u |
- | Maghreb | 0 | https://iptv-org.github.io/iptv/regions/maghreb.m3u |
- | Middle East | 0 | https://iptv-org.github.io/iptv/regions/mideast.m3u |
- | Middle East and North Africa | 0 | https://iptv-org.github.io/iptv/regions/mena.m3u |
- | Nordics | 0 | https://iptv-org.github.io/iptv/regions/nord.m3u |
- | North America | 1 | https://iptv-org.github.io/iptv/regions/noram.m3u |
- | Northern America | 1 | https://iptv-org.github.io/iptv/regions/nam.m3u |
- | Oceania | 0 | https://iptv-org.github.io/iptv/regions/oce.m3u |
- | South America | 0 | https://iptv-org.github.io/iptv/regions/southam.m3u |
- | South Asia | 1 | https://iptv-org.github.io/iptv/regions/sas.m3u |
- | Sub-Saharan Africa | 0 | https://iptv-org.github.io/iptv/regions/ssa.m3u |
- | West Africa | 0 | https://iptv-org.github.io/iptv/regions/wafr.m3u |
- | Undefined | 2 | https://iptv-org.github.io/iptv/regions/undefined.m3u |
-
-
+- Africa https://iptv-org.github.io/iptv/regions/afr.m3u
+- Americas https://iptv-org.github.io/iptv/regions/amer.m3u
+- Arab world https://iptv-org.github.io/iptv/regions/arab.m3u
+- Asia https://iptv-org.github.io/iptv/regions/asia.m3u
+- Asia-Pacific https://iptv-org.github.io/iptv/regions/apac.m3u
+- Association of Southeast Asian Nations https://iptv-org.github.io/iptv/regions/asean.m3u
+- Caribbean https://iptv-org.github.io/iptv/regions/carib.m3u
+- Central America https://iptv-org.github.io/iptv/regions/cenamer.m3u
+- Central Asia https://iptv-org.github.io/iptv/regions/cas.m3u
+- Commonwealth of Independent States https://iptv-org.github.io/iptv/regions/cis.m3u
+- Europe https://iptv-org.github.io/iptv/regions/eur.m3u
+- Europe, the Middle East and Africa https://iptv-org.github.io/iptv/regions/emea.m3u
+- Hispanic America https://iptv-org.github.io/iptv/regions/hispam.m3u
+- Latin America https://iptv-org.github.io/iptv/regions/latam.m3u
+- Latin America and the Caribbean https://iptv-org.github.io/iptv/regions/lac.m3u
+- Maghreb https://iptv-org.github.io/iptv/regions/maghreb.m3u
+- Middle East https://iptv-org.github.io/iptv/regions/mideast.m3u
+- Middle East and North Africa https://iptv-org.github.io/iptv/regions/mena.m3u
+- Nordics https://iptv-org.github.io/iptv/regions/nord.m3u
+- North America https://iptv-org.github.io/iptv/regions/noram.m3u
+- Northern America https://iptv-org.github.io/iptv/regions/nam.m3u
+- Oceania https://iptv-org.github.io/iptv/regions/oce.m3u
+- South America https://iptv-org.github.io/iptv/regions/southam.m3u
+- South Asia https://iptv-org.github.io/iptv/regions/sas.m3u
+- Sub-Saharan Africa https://iptv-org.github.io/iptv/regions/ssa.m3u
+- West Africa https://iptv-org.github.io/iptv/regions/wafr.m3u
diff --git a/tests/__data__/input/data/cities.json b/tests/__data__/input/data/cities.json
index 7bbd99d9b1..2bf660f0ef 100644
--- a/tests/__data__/input/data/cities.json
+++ b/tests/__data__/input/data/cities.json
@@ -1 +1 @@
-[{"country":"AD","subdivision":"AD-02","code":"ADCAN","name":"Canillo","wikidata_id":"Q386802"},{"country":"CA","subdivision":"CA-ON","code":"CAAAC","name":"Ailsa Craig","wikidata_id":"Q65963197"}]
\ No newline at end of file
+[{"country":"AD","subdivision":"AD-02","code":"ADCAN","name":"Canillo","wikidata_id":"Q386802"},{"country":"CA","subdivision":"CA-ON","code":"CAAAC","name":"Ailsa Craig","wikidata_id":"Q65963197"},{"country":"HK","subdivision":null,"code":"HK9SK","name":"Sai Kung","wikidata_id":"Q206377"}]
\ No newline at end of file
diff --git a/tests/__data__/input/data/feeds.json b/tests/__data__/input/data/feeds.json
index 5383440571..1c2ce8806c 100644
--- a/tests/__data__/input/data/feeds.json
+++ b/tests/__data__/input/data/feeds.json
@@ -148,7 +148,7 @@
"name": "SD",
"is_main": true,
"broadcast_area": [
- "ct/CAAAC"
+ "s/CA-ON"
],
"languages": [
"fru"
@@ -180,7 +180,7 @@
"name": "SD",
"is_main": true,
"broadcast_area": [
- "r/INT"
+ "r/WW"
],
"languages": [
"nld"
@@ -805,7 +805,7 @@
"name": "SD",
"is_main": true,
"broadcast_area": [
- "r/INT"
+ "r/WW"
],
"languages": [],
"timezones": [
diff --git a/tests/__data__/input/data/regions.json b/tests/__data__/input/data/regions.json
index e1f7b1e672..9646031669 100644
--- a/tests/__data__/input/data/regions.json
+++ b/tests/__data__/input/data/regions.json
@@ -1 +1 @@
-[{"code":"AFR","name":"Africa","countries":["AO","BF","BI","BJ","BW","CD","CF","CG","CI","CM","CV","DJ","DZ","EG","EH","ER","ET","GA","GH","GM","GN","GQ","GW","KE","KM","LR","LS","LY","MA","MG","ML","MR","MU","MW","MZ","NA","NE","NG","RE","RW","SC","SD","SH","SL","SN","SO","SS","ST","SZ","TD","TF","TG","TN","TZ","UG","YT","ZA","ZM","ZW"]},{"code":"AMER","name":"Americas","countries":["AG","AI","AR","AW","BB","BL","BM","BO","BR","BS","BV","BZ","CA","CL","CO","CR","CU","CW","DM","DO","EC","FK","GD","GF","GL","GP","GS","GT","GY","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PE","PM","PR","PY","SR","SV","SX","TC","TT","US","UY","VC","VE","VG","VI"]},{"code":"APAC","name":"Asia-Pacific","countries":["AF","AS","AU","BD","BN","BT","CK","CN","FJ","FM","GU","ID","IN","JP","KH","KI","KP","KR","LA","LK","MH","MM","MN","MP","MV","MY","NC","NF","NP","NR","NU","NZ","PF","PG","PH","PK","PN","PW","SB","SG","TH","TK","TL","TO","TV","TW","VN","VU","WF","WS"]},{"code":"ARAB","name":"Arab world","countries":["AE","BH","DJ","DZ","EG","IQ","JO","KM","KW","LB","LY","MA","MR","OM","PS","QA","SA","SD","SO","SY","TN","YE"]},{"code":"ASEAN","name":"Association of Southeast Asian Nations","countries":["BN","KH","ID","LA","MY","MM","PH","SG","TH","VN"]},{"code":"ASIA","name":"Asia","countries":["AE","AF","AM","AZ","BD","BH","BN","BT","CN","CY","GE","ID","IL","IN","IQ","IR","JO","JP","KG","KH","KP","KR","KW","KZ","LA","LB","LK","MM","MN","MV","MY","NP","OM","PH","PK","PS","QA","RU","SA","SG","SY","TH","TJ","TL","TM","TR","TW","UZ","VN","YE"]},{"code":"CARIB","name":"Caribbean","countries":["AG","AI","AW","BB","BL","BS","CU","CW","DM","DO","GD","GP","HT","JM","KN","KY","LC","MF","MQ","MS","PR","SX","TC","TT","VC","VG","VI"]},{"code":"CAS","name":"Central Asia","countries":["KG","KZ","TJ","TM","UZ"]},{"code":"CENAMER","name":"Central America","countries":["BZ","CR","SV","GT","HN","NI","PA"]},{"code":"CIS","name":"Commonwealth of Independent States","countries":["AM","AZ","BY","KG","KZ","MD","RU","TJ","UZ"]},{"code":"EMEA","name":"Europe, the Middle East and Africa","countries":["AD","AE","AL","AM","AO","AT","AZ","BA","BE","BF","BG","BH","BI","BJ","BW","BY","CD","CF","CG","CH","CI","CM","CV","CY","CZ","DE","DJ","DK","DZ","EE","EG","EH","ER","ES","ET","FI","FR","GA","GE","GH","GM","GN","GQ","GR","GW","HR","HU","IE","IQ","IR","IS","IT","JO","KE","KM","KW","KZ","LB","LI","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MG","MK","ML","MR","MT","MU","MW","MZ","NA","NE","NG","NL","NO","OM","PL","PS","PT","QA","RE","RO","RS","RU","RW","SA","SC","SD","SE","SH","SI","SK","SL","SM","SN","SO","SS","ST","SY","SZ","TD","TF","TG","TN","TR","TZ","UA","UG","UK","VA","YE","YT","ZA","ZM","ZW"]},{"code":"EUR","name":"Europe","countries":["AD","AL","AM","AT","AZ","BA","BE","BG","BY","CH","CY","CZ","DE","DK","EE","ES","FI","FR","GE","GR","HR","HU","IE","IS","IT","KZ","LI","LT","LU","LV","MC","MD","ME","MK","MT","NL","NO","PL","PT","RO","RS","RU","SE","SI","SK","SM","TR","UA","UK","VA"]},{"code":"HISPAM","name":"Hispanic America","countries":["AR","BO","CL","CO","CR","CU","DO","EC","GT","HN","MX","NI","PA","PE","PR","PY","SV","UY","VE"]},{"code":"LAC","name":"Latin America and the Caribbean","countries":["AG","AI","AR","AW","BB","BL","BO","BR","BS","CL","CO","CR","CU","CW","DM","DO","EC","GD","GF","GP","GT","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PE","PR","PY","SV","SX","TC","TT","UY","VC","VE","VG","VI"]},{"code":"LATAM","name":"Latin America","countries":["AR","BL","BO","BR","CL","CO","CR","CU","DO","EC","GF","GP","GT","HN","HT","MF","MQ","MX","NI","PA","PE","PR","PY","SV","UY","VE"]},{"code":"MAGHREB","name":"Maghreb","countries":["DZ","LY","MA","MR","TN"]},{"code":"MENA","name":"Middle East and North Africa","countries":["AE","BH","CY","DJ","DZ","EG","EH","IL","IQ","IR","JO","KW","LB","LY","MA","OM","PS","QA","SA","SD","SY","TN","TR","YE"]},{"code":"MIDEAST","name":"Middle East","countries":["AE","BH","CY","EG","IL","IQ","IR","JO","KW","LB","OM","PS","QA","SA","SY","TR","YE"]},{"code":"NAM","name":"Northern America","countries":["BM","CA","GL","PM","US"]},{"code":"NORAM","name":"North America","countries":["AG","AI","AW","BB","BL","BM","BS","BZ","CA","CR","CU","CW","DM","DO","GD","GL","GP","GT","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PM","PR","SV","SX","TC","TT","US","VC","VG","VI"]},{"code":"NORD","name":"Nordics","countries":["AX","DK","FO","FI","IS","NO","SE"]},{"code":"OCE","name":"Oceania","countries":["AS","AU","CK","FJ","FM","GU","KI","MH","MP","NC","NF","NR","NU","NZ","PF","PG","PN","PW","SB","TK","TO","TV","VU","WF","WS"]},{"code":"SAS","name":"South Asia","countries":["AF","BD","BT","IN","LK","MV","NP","PK"]},{"code":"SOUTHAM","name":"South America","countries":["AR","BO","BR","CL","CO","EC","PY","PE","UY","VE","BV","FK","GF","GY","GS","SR"]},{"code":"SSA","name":"Sub-Saharan Africa","countries":["AO","BF","BI","BJ","BW","CD","CF","CG","CI","CM","CV","DJ","ER","ET","GA","GH","GM","GN","GQ","GW","KE","KM","LR","LS","MG","ML","MR","MU","MW","MZ","NA","NE","NG","RW","SC","SD","SL","SN","SO","SS","ST","SZ","TD","TG","TZ","UG","ZA","ZM","ZW"]},{"code":"WAFR","name":"West Africa","countries":["BF","BJ","CI","CV","GH","GM","GN","GW","LR","ML","MR","NE","NG","SH","SL","SN","TG"]}]
\ No newline at end of file
+[{"code":"AFR","name":"Africa","countries":["AO","BF","BI","BJ","BW","CD","CF","CG","CI","CM","CV","DJ","DZ","EG","EH","ER","ET","GA","GH","GM","GN","GQ","GW","KE","KM","LR","LS","LY","MA","MG","ML","MR","MU","MW","MZ","NA","NE","NG","RE","RW","SC","SD","SH","SL","SN","SO","SS","ST","SZ","TD","TF","TG","TN","TZ","UG","YT","ZA","ZM","ZW"]},{"code":"AMER","name":"Americas","countries":["AG","AI","AR","AW","BB","BL","BM","BO","BR","BS","BV","BZ","CA","CL","CO","CR","CU","CW","DM","DO","EC","FK","GD","GF","GL","GP","GS","GT","GY","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PE","PM","PR","PY","SR","SV","SX","TC","TT","US","UY","VC","VE","VG","VI"]},{"code":"APAC","name":"Asia-Pacific","countries":["AF","AS","AU","BD","BN","BT","CK","CN","FJ","FM","GU","ID","IN","JP","KH","KI","KP","KR","LA","LK","MH","MM","MN","MP","MV","MY","NC","NF","NP","NR","NU","NZ","PF","PG","PH","PK","PN","PW","SB","SG","TH","TK","TL","TO","TV","TW","VN","VU","WF","WS"]},{"code":"ARAB","name":"Arab world","countries":["AE","BH","DJ","DZ","EG","IQ","JO","KM","KW","LB","LY","MA","MR","OM","PS","QA","SA","SD","SO","SY","TN","YE"]},{"code":"ASEAN","name":"Association of Southeast Asian Nations","countries":["BN","KH","ID","LA","MY","MM","PH","SG","TH","VN"]},{"code":"ASIA","name":"Asia","countries":["AE","AF","AM","AZ","BD","BH","BN","BT","CN","CY","GE","ID","IL","IN","IQ","IR","JO","JP","KG","KH","KP","KR","KW","KZ","LA","LB","LK","MM","MN","MV","MY","NP","OM","PH","PK","PS","QA","RU","SA","SG","SY","TH","TJ","TL","TM","TR","TW","UZ","VN","YE"]},{"code":"CARIB","name":"Caribbean","countries":["AG","AI","AW","BB","BL","BS","CU","CW","DM","DO","GD","GP","HT","JM","KN","KY","LC","MF","MQ","MS","PR","SX","TC","TT","VC","VG","VI"]},{"code":"CAS","name":"Central Asia","countries":["KG","KZ","TJ","TM","UZ"]},{"code":"CENAMER","name":"Central America","countries":["BZ","CR","SV","GT","HN","NI","PA"]},{"code":"CIS","name":"Commonwealth of Independent States","countries":["AM","AZ","BY","KG","KZ","MD","RU","TJ","UZ"]},{"code":"EMEA","name":"Europe, the Middle East and Africa","countries":["AD","AE","AL","AM","AO","AT","AZ","BA","BE","BF","BG","BH","BI","BJ","BW","BY","CD","CF","CG","CH","CI","CM","CV","CY","CZ","DE","DJ","DK","DZ","EE","EG","EH","ER","ES","ET","FI","FR","GA","GE","GH","GM","GN","GQ","GR","GW","HR","HU","IE","IQ","IR","IS","IT","JO","KE","KM","KW","KZ","LB","LI","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MG","MK","ML","MR","MT","MU","MW","MZ","NA","NE","NG","NL","NO","OM","PL","PS","PT","QA","RE","RO","RS","RU","RW","SA","SC","SD","SE","SH","SI","SK","SL","SM","SN","SO","SS","ST","SY","SZ","TD","TF","TG","TN","TR","TZ","UA","UG","UK","VA","YE","YT","ZA","ZM","ZW"]},{"code":"WW","name":"Worldwide","countries":["AD","AE","AF","AG","AI","AL","AM","AO","AQ","AR","AS","AT","AU","AW","AX","AZ","BA","BB","BD","BE","BF","BG","BH","BI","BJ","BL","BM","BN","BO","BQ","BR","BS","BT","BV","BW","BY","BZ","CA","CC","CD","CF","CG","CH","CI","CK","CL","CM","CN","CO","CR","CU","CV","CW","CX","CY","CZ","DE","DJ","DK","DM","DO","DZ","EC","EE","EG","EH","ER","ES","ET","FI","FJ","FK","FM","FO","FR","GA","UK","GD","GE","GF","GG","GH","GI","GL","GM","GN","GP","GQ","GR","GS","GT","GU","GW","GY","HK","HM","HN","HR","HT","HU","ID","IE","IL","IM","IN","IO","IQ","IR","IS","IT","JE","JM","JO","JP","KE","KG","KH","KI","KM","KN","KP","KR","KW","KY","KZ","LA","LB","LC","LI","LK","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MF","MG","MH","MK","ML","MM","MN","MO","MP","MQ","MR","MS","MT","MU","MV","MW","MX","MY","MZ","NA","NC","NE","NF","NG","NI","NL","NO","NP","NR","NU","NZ","OM","PA","PE","PF","PG","PH","PK","PL","PM","PN","PR","PS","PT","PW","PY","QA","RE","RO","RS","RU","RW","SA","SB","SC","SD","SE","SG","SH","SI","SJ","SK","SL","SM","SN","SO","SR","SS","ST","SV","SX","SY","SZ","TC","TD","TF","TG","TH","TJ","TK","TL","TM","TN","TO","TR","TT","TV","TW","TZ","UA","UG","UM","US","UY","UZ","VA","VC","VE","VG","VI","VN","VU","WF","WS","XK","YE","YT","ZA","ZM","ZW"]},{"code":"EUR","name":"Europe","countries":["AD","AL","AM","AT","AZ","BA","BE","BG","BY","CH","CY","CZ","DE","DK","EE","ES","FI","FR","GE","GR","HR","HU","IE","IS","IT","KZ","LI","LT","LU","LV","MC","MD","ME","MK","MT","NL","NO","PL","PT","RO","RS","RU","SE","SI","SK","SM","TR","UA","UK","VA"]},{"code":"HISPAM","name":"Hispanic America","countries":["AR","BO","CL","CO","CR","CU","DO","EC","GT","HN","MX","NI","PA","PE","PR","PY","SV","UY","VE"]},{"code":"LAC","name":"Latin America and the Caribbean","countries":["AG","AI","AR","AW","BB","BL","BO","BR","BS","CL","CO","CR","CU","CW","DM","DO","EC","GD","GF","GP","GT","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PE","PR","PY","SV","SX","TC","TT","UY","VC","VE","VG","VI"]},{"code":"LATAM","name":"Latin America","countries":["AR","BL","BO","BR","CL","CO","CR","CU","DO","EC","GF","GP","GT","HN","HT","MF","MQ","MX","NI","PA","PE","PR","PY","SV","UY","VE"]},{"code":"MAGHREB","name":"Maghreb","countries":["DZ","LY","MA","MR","TN"]},{"code":"MENA","name":"Middle East and North Africa","countries":["AE","BH","CY","DJ","DZ","EG","EH","IL","IQ","IR","JO","KW","LB","LY","MA","OM","PS","QA","SA","SD","SY","TN","TR","YE"]},{"code":"MIDEAST","name":"Middle East","countries":["AE","BH","CY","EG","IL","IQ","IR","JO","KW","LB","OM","PS","QA","SA","SY","TR","YE"]},{"code":"NAM","name":"Northern America","countries":["BM","CA","GL","PM","US"]},{"code":"NORAM","name":"North America","countries":["AG","AI","AW","BB","BL","BM","BS","BZ","CA","CR","CU","CW","DM","DO","GD","GL","GP","GT","HN","HT","JM","KN","KY","LC","MF","MQ","MS","MX","NI","PA","PM","PR","SV","SX","TC","TT","US","VC","VG","VI"]},{"code":"NORD","name":"Nordics","countries":["AX","DK","FO","FI","IS","NO","SE"]},{"code":"OCE","name":"Oceania","countries":["AS","AU","CK","FJ","FM","GU","KI","MH","MP","NC","NF","NR","NU","NZ","PF","PG","PN","PW","SB","TK","TO","TV","VU","WF","WS"]},{"code":"SAS","name":"South Asia","countries":["AF","BD","BT","IN","LK","MV","NP","PK"]},{"code":"SOUTHAM","name":"South America","countries":["AR","BO","BR","CL","CO","EC","PY","PE","UY","VE","BV","FK","GF","GY","GS","SR"]},{"code":"SSA","name":"Sub-Saharan Africa","countries":["AO","BF","BI","BJ","BW","CD","CF","CG","CI","CM","CV","DJ","ER","ET","GA","GH","GM","GN","GQ","GW","KE","KM","LR","LS","MG","ML","MR","MU","MW","MZ","NA","NE","NG","RW","SC","SD","SL","SN","SO","SS","ST","SZ","TD","TG","TZ","UG","ZA","ZM","ZW"]},{"code":"WAFR","name":"West Africa","countries":["BF","BJ","CI","CV","GH","GM","GN","GW","LR","ML","MR","NE","NG","SH","SL","SN","TG"]}]
\ No newline at end of file
diff --git a/tests/__data__/input/readme_update/.readme/config.json b/tests/__data__/input/readme_update/.readme/config.json
deleted file mode 100644
index e02806b642..0000000000
--- a/tests/__data__/input/readme_update/.readme/config.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "build" : "tests/__data__/output/playlists.md",
- "files" : ["tests/__data__/output/.readme/template.md"]
-}
\ No newline at end of file
diff --git a/tests/__data__/input/readme_update/.readme/template.md b/tests/__data__/input/readme_update/.readme/template.md
deleted file mode 100644
index d1291f2f49..0000000000
--- a/tests/__data__/input/readme_update/.readme/template.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# Playlists
-
-### Grouped by category
-
-
-Expand
-
-
-Playlist in which each channel has its _category_ as a group title:
-
-```
-https://iptv-org.github.io/iptv/index.category.m3u
-```
-
-Same thing, but split up into separate files:
-
-
-#include "tests/__data__/output/.readme/_categories.md"
-
-
-
-### Grouped by language
-
-
-Expand
-
-
-Playlist in which each channel has its _language_ as a group title:
-
-```
-https://iptv-org.github.io/iptv/index.language.m3u
-```
-
-Same thing, but split up into separate files:
-
-
-#include "tests/__data__/output/.readme/_languages.md"
-
-
-
-### Grouped by country
-
-
-Expand
-
-
-Playlist in which each channel has its _country_ as a group title:
-
-```
-https://iptv-org.github.io/iptv/index.country.m3u
-```
-
-Same thing, but split up into separate files:
-
-
-#include "tests/__data__/output/.readme/_countries.md"
-
-
-
-### Grouped by subdivision
-
-
-Expand
-
-
-
-#include "tests/__data__/output/.readme/_subdivisions.md"
-
-
-
-### Grouped by region
-
-
-Expand
-
-
-Playlist in which each channel has its _region_ as a group title:
-
-```
-https://iptv-org.github.io/iptv/index.region.m3u
-```
-
-Same thing, but split up into separate files:
-
-
-#include "tests/__data__/output/.readme/_regions.md"
-
-
-
-### Grouped by sources
-
-Playlists in which channels are grouped by broadcast source.
-
-
-Expand
-
-
-To use the playlist, simply replace `` in the link below with the name of one of the files in the [streams](streams) folder.
-
-```
-https://iptv-org.github.io/iptv/sources/.m3u
-```
-
-
-
-Also, any of our internal playlists are available in raw form (without any filtering or sorting) at this link:
-
-```
-https://iptv-org.github.io/iptv/raw/.m3u
-```
diff --git a/tests/__data__/input/readme_update/generators.log b/tests/__data__/input/readme_update/generators.log
index a2e36eb1fe..de76b7f764 100644
--- a/tests/__data__/input/readme_update/generators.log
+++ b/tests/__data__/input/readme_update/generators.log
@@ -38,9 +38,12 @@
{"type":"country","filepath":"countries/cg.m3u","count":1}
{"type":"country","filepath":"countries/ro.m3u","count":1}
{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":1}
+{"type":"city","filepath":"cities/adcan.m3u","count":1}
+{"type":"city","filepath":"cities/hk9sk.m3u","count":1}
{"type":"country","filepath":"countries/ru.m3u","count":2}
{"type":"country","filepath":"countries/rw.m3u","count":1}
{"type":"country","filepath":"countries/re.m3u","count":1}
+{"type":"country","filepath":"countries/int.m3u","count":3}
{"type":"country","filepath":"countries/undefined.m3u","count":2}
{"type":"country","filepath":"countries/bl.m3u","count":1}
{"type":"country","filepath":"countries/sh.m3u","count":1}
From 832acb11c29a774ec2f4f935b290255a0d524aef Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sun, 24 Aug 2025 22:42:59 +0300
Subject: [PATCH 13/15] Update update.test.ts
---
tests/commands/readme/update.test.ts | 16 ++++------------
1 file changed, 4 insertions(+), 12 deletions(-)
diff --git a/tests/commands/readme/update.test.ts b/tests/commands/readme/update.test.ts
index 87af269728..8031c8737e 100644
--- a/tests/commands/readme/update.test.ts
+++ b/tests/commands/readme/update.test.ts
@@ -2,19 +2,11 @@ import { pathToFileURL } from 'node:url'
import { execSync } from 'child_process'
import fs from 'fs-extra'
-const ENV_VAR = 'cross-env DATA_DIR=tests/__data__/input/data LOGS_DIR=tests/__data__/input/readme_update README_DIR=tests/__data__/output/.readme'
+const ENV_VAR =
+ 'cross-env DATA_DIR=tests/__data__/input/data LOGS_DIR=tests/__data__/input/readme_update ROOT_DIR=tests/__data__/output'
beforeEach(() => {
fs.emptyDirSync('tests/__data__/output')
- fs.mkdirSync('tests/__data__/output/.readme')
- fs.copyFileSync(
- 'tests/__data__/input/readme_update/.readme/config.json',
- 'tests/__data__/output/.readme/config.json'
- )
- fs.copyFileSync(
- 'tests/__data__/input/readme_update/.readme/template.md',
- 'tests/__data__/output/.readme/template.md'
- )
})
describe('readme:update', () => {
@@ -23,8 +15,8 @@ describe('readme:update', () => {
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
- expect(content('tests/__data__/output/playlists.md')).toEqual(
- content('tests/__data__/expected/readme_update/playlists.md')
+ expect(content('tests/__data__/output/PLAYLISTS.md')).toEqual(
+ content('tests/__data__/expected/readme_update/PLAYLISTS.md')
)
})
})
From b85eca12be87f14459040baee96328aad787d77d Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sun, 24 Aug 2025 22:43:13 +0300
Subject: [PATCH 14/15] Update .readme/
---
.readme/.gitignore | 3 +--
.readme/config.json | 4 ----
.readme/template.md | 34 +++++++---------------------------
3 files changed, 8 insertions(+), 33 deletions(-)
delete mode 100644 .readme/config.json
diff --git a/.readme/.gitignore b/.readme/.gitignore
index e57faaff68..a2ac441348 100644
--- a/.readme/.gitignore
+++ b/.readme/.gitignore
@@ -1,5 +1,4 @@
_categories.md
_countries.md
_languages.md
-_regions.md
-_subdivisions.md
\ No newline at end of file
+_regions.md
\ No newline at end of file
diff --git a/.readme/config.json b/.readme/config.json
deleted file mode 100644
index ff55cfec76..0000000000
--- a/.readme/config.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "build" : "PLAYLISTS.md",
- "files" : ["./.readme/template.md"]
-}
\ No newline at end of file
diff --git a/.readme/template.md b/.readme/template.md
index dc5cad228d..c7b26655ea 100644
--- a/.readme/template.md
+++ b/.readme/template.md
@@ -40,16 +40,17 @@ Same thing, but split up into separate files:
-### Grouped by country
+### Grouped by broadcast area
-Playlists in which channels are grouped by country for which they are broadcasted.
+Playlists in which channels are grouped by broadcast area.
Expand
-
+
+#### Countries
```
-https://iptv-org.github.io/iptv/index.country.m3u
+https://iptv-org.github.io/iptv/index.countries.m3u
```
Same thing, but split up into separate files:
@@ -57,31 +58,10 @@ Same thing, but split up into separate files:
#include "./.readme/_countries.md"
-
-
-### Grouped by subdivision
-
-Playlists in which channels are grouped by subdivision for which they are broadcasted.
-
-
-Expand
-
-
-
-#include "./.readme/_subdivisions.md"
-
-
-
-### Grouped by region
-
-Playlists in which channels are grouped by the region for which they are broadcasted.
-
-
-Expand
-
+#### Regions
```
-https://iptv-org.github.io/iptv/index.region.m3u
+https://iptv-org.github.io/iptv/index.regions.m3u
```
Same thing, but split up into separate files:
From 8f9ea06133fa26e46703db0d71c0ba0edf79e422 Mon Sep 17 00:00:00 2001
From: freearhey <7253922+freearhey@users.noreply.github.com>
Date: Sun, 24 Aug 2025 22:43:25 +0300
Subject: [PATCH 15/15] Update scripts
---
scripts/commands/playlist/generate.ts | 11 +-
scripts/commands/readme/update.ts | 55 +++--
scripts/core/markdown.ts | 44 ++--
scripts/generators/citiesGenerator.ts | 43 ++++
scripts/generators/countriesGenerator.ts | 12 ++
scripts/generators/index.ts | 1 +
scripts/generators/indexCountryGenerator.ts | 8 +
scripts/generators/indexRegionGenerator.ts | 2 +
scripts/generators/regionsGenerator.ts | 2 +
scripts/models/broadcastArea.ts | 5 +-
scripts/models/feed.ts | 18 +-
scripts/models/region.ts | 4 +
scripts/models/stream.ts | 20 +-
.../{categoryTable.ts => categoriesTable.ts} | 31 +--
scripts/tables/countriesTable.ts | 189 ++++++++++++++++++
scripts/tables/countryTable.ts | 65 ------
scripts/tables/index.ts | 9 +-
.../{languageTable.ts => languagesTable.ts} | 23 ++-
scripts/tables/regionTable.ts | 62 ------
scripts/tables/regionsTable.ts | 52 +++++
scripts/tables/subdivisionTable.ts | 72 -------
21 files changed, 453 insertions(+), 275 deletions(-)
create mode 100644 scripts/generators/citiesGenerator.ts
rename scripts/tables/{categoryTable.ts => categoriesTable.ts} (61%)
create mode 100644 scripts/tables/countriesTable.ts
delete mode 100644 scripts/tables/countryTable.ts
rename scripts/tables/{languageTable.ts => languagesTable.ts} (68%)
delete mode 100644 scripts/tables/regionTable.ts
create mode 100644 scripts/tables/regionsTable.ts
delete mode 100644 scripts/tables/subdivisionTable.ts
diff --git a/scripts/commands/playlist/generate.ts b/scripts/commands/playlist/generate.ts
index 7165c4b980..58677a59c7 100644
--- a/scripts/commands/playlist/generate.ts
+++ b/scripts/commands/playlist/generate.ts
@@ -16,6 +16,7 @@ import {
LanguagesGenerator,
RegionsGenerator,
SourcesGenerator,
+ CitiesGenerator,
IndexGenerator,
RawGenerator
} from '../../generators'
@@ -36,7 +37,8 @@ async function main() {
subdivisions,
categories,
countries,
- regions
+ regions,
+ cities
}: DataProcessorData = processor.process(data)
logger.info('loading streams...')
@@ -90,6 +92,13 @@ async function main() {
logFile
}).generate()
+ logger.info('generating cities/...')
+ await new CitiesGenerator({
+ cities,
+ streams,
+ logFile
+ }).generate()
+
logger.info('generating regions/...')
await new RegionsGenerator({
streams,
diff --git a/scripts/commands/readme/update.ts b/scripts/commands/readme/update.ts
index 86eddfe365..d47f6ba48a 100644
--- a/scripts/commands/readme/update.ts
+++ b/scripts/commands/readme/update.ts
@@ -1,32 +1,47 @@
-import { Logger } from '@freearhey/core'
-import {
- CategoryTable,
- CountryTable,
- LanguageTable,
- RegionTable,
- SubdivisionTable
-} from '../../tables'
-import { Markdown } from '../../core'
-import { README_DIR } from '../../constants'
-import path from 'path'
+import { CategoriesTable, CountriesTable, LanguagesTable, RegionsTable } from '../../tables'
+import { DataLoader, DataProcessor, Markdown } from '../../core'
+import { DataProcessorData } from '../../types/dataProcessor'
+import { DataLoaderData } from '../../types/dataLoader'
+import { README_DIR, DATA_DIR, ROOT_DIR } from '../../constants'
+import { Logger, Storage } from '@freearhey/core'
async function main() {
const logger = new Logger()
+ const dataStorage = new Storage(DATA_DIR)
+ const processor = new DataProcessor()
+ const loader = new DataLoader({ storage: dataStorage })
+ const data: DataLoaderData = await loader.load()
+ const {
+ subdivisionsKeyByCode,
+ languagesKeyByCode,
+ countriesKeyByCode,
+ categoriesKeyById,
+ subdivisions,
+ countries,
+ regions,
+ cities
+ }: DataProcessorData = processor.process(data)
logger.info('creating category table...')
- await new CategoryTable().make()
+ await new CategoriesTable({ categoriesKeyById }).make()
logger.info('creating language table...')
- await new LanguageTable().make()
- logger.info('creating country table...')
- await new CountryTable().make()
- logger.info('creating subdivision table...')
- await new SubdivisionTable().make()
+ await new LanguagesTable({ languagesKeyByCode }).make()
+ logger.info('creating countires table...')
+ await new CountriesTable({
+ countriesKeyByCode,
+ subdivisionsKeyByCode,
+ subdivisions,
+ countries,
+ cities
+ }).make()
logger.info('creating region table...')
- await new RegionTable().make()
+ await new RegionsTable({ regions }).make()
logger.info('updating playlists.md...')
- const configPath = path.join(README_DIR, 'config.json')
- const playlists = new Markdown(configPath)
+ const playlists = new Markdown({
+ build: `${ROOT_DIR}/PLAYLISTS.md`,
+ template: `${README_DIR}/template.md`
+ })
playlists.compile()
}
diff --git a/scripts/core/markdown.ts b/scripts/core/markdown.ts
index 36834d8b7b..e229999409 100644
--- a/scripts/core/markdown.ts
+++ b/scripts/core/markdown.ts
@@ -1,33 +1,37 @@
import fs from 'fs'
import path from 'path'
-export class Markdown {
- filepath: string
+type MarkdownConfig = {
+ build: string
+ template: string
+}
- constructor(filepath: string) {
- this.filepath = filepath
+export class Markdown {
+ build: string
+ template: string
+
+ constructor(config: MarkdownConfig) {
+ this.build = config.build
+ this.template = config.template
}
compile() {
- const config = JSON.parse(fs.readFileSync(this.filepath, 'utf8'))
- const workingDir = process.cwd()
-
- config.files.forEach((templateFile: string) => {
- const templatePath = path.resolve(workingDir, templateFile)
- const content = fs.readFileSync(templatePath, 'utf8')
- const processedContent = this.processIncludes(content, workingDir)
-
- if (config.build) {
- const outputPath = path.resolve(workingDir, config.build)
- fs.writeFileSync(outputPath, processedContent, 'utf8')
- }
- })
+ const workingDir = process.cwd()
+
+ const templatePath = path.resolve(workingDir, this.template)
+ const template = fs.readFileSync(templatePath, 'utf8')
+ const processedContent = this.processIncludes(template, workingDir)
+
+ if (this.build) {
+ const outputPath = path.resolve(workingDir, this.build)
+ fs.writeFileSync(outputPath, processedContent, 'utf8')
+ }
}
- private processIncludes(content: string, baseDir: string): string {
+ private processIncludes(template: string, baseDir: string): string {
const includeRegex = /#include\s+"([^"]+)"/g
-
- return content.replace(includeRegex, (match, includePath) => {
+
+ return template.replace(includeRegex, (match, includePath) => {
try {
const fullPath = path.resolve(baseDir, includePath)
const includeContent = fs.readFileSync(fullPath, 'utf8')
diff --git a/scripts/generators/citiesGenerator.ts b/scripts/generators/citiesGenerator.ts
new file mode 100644
index 0000000000..7d0891fbc8
--- /dev/null
+++ b/scripts/generators/citiesGenerator.ts
@@ -0,0 +1,43 @@
+import { City, Stream, Playlist } from '../models'
+import { Collection, Storage, File } from '@freearhey/core'
+import { PUBLIC_DIR, EOL } from '../constants'
+import { Generator } from './generator'
+
+type CitiesGeneratorProps = {
+ streams: Collection
+ cities: Collection
+ logFile: File
+}
+
+export class CitiesGenerator implements Generator {
+ streams: Collection
+ cities: Collection
+ storage: Storage
+ logFile: File
+
+ constructor({ streams, cities, logFile }: CitiesGeneratorProps) {
+ this.streams = streams.clone()
+ this.cities = cities
+ this.storage = new Storage(PUBLIC_DIR)
+ this.logFile = logFile
+ }
+
+ async generate(): Promise {
+ const streams = this.streams
+ .orderBy((stream: Stream) => stream.getTitle())
+ .filter((stream: Stream) => stream.isSFW())
+
+ this.cities.forEach(async (city: City) => {
+ const cityStreams = streams.filter((stream: Stream) => stream.isBroadcastInCity(city))
+
+ if (cityStreams.isEmpty()) return
+
+ const playlist = new Playlist(cityStreams, { public: true })
+ const filepath = `cities/${city.code.toLowerCase()}.m3u`
+ await this.storage.save(filepath, playlist.toString())
+ this.logFile.append(
+ JSON.stringify({ type: 'city', filepath, count: playlist.streams.count() }) + EOL
+ )
+ })
+ }
+}
diff --git a/scripts/generators/countriesGenerator.ts b/scripts/generators/countriesGenerator.ts
index 4cd539deaf..39d7612582 100644
--- a/scripts/generators/countriesGenerator.ts
+++ b/scripts/generators/countriesGenerator.ts
@@ -41,6 +41,18 @@ export class CountriesGenerator implements Generator {
)
})
+ const internationalStreams = streams.filter((stream: Stream) => stream.isInternational())
+ const internationalPlaylist = new Playlist(internationalStreams, { public: true })
+ const internationalFilepath = 'countries/int.m3u'
+ await this.storage.save(internationalFilepath, internationalPlaylist.toString())
+ this.logFile.append(
+ JSON.stringify({
+ type: 'country',
+ filepath: internationalFilepath,
+ count: internationalPlaylist.streams.count()
+ }) + EOL
+ )
+
const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea())
const undefinedPlaylist = new Playlist(undefinedStreams, { public: true })
const undefinedFilepath = 'countries/undefined.m3u'
diff --git a/scripts/generators/index.ts b/scripts/generators/index.ts
index e01127ba12..7f09a6715b 100644
--- a/scripts/generators/index.ts
+++ b/scripts/generators/index.ts
@@ -1,4 +1,5 @@
export * from './categoriesGenerator'
+export * from './citiesGenerator'
export * from './countriesGenerator'
export * from './indexCategoryGenerator'
export * from './indexCountryGenerator'
diff --git a/scripts/generators/indexCountryGenerator.ts b/scripts/generators/indexCountryGenerator.ts
index 7384dd8836..b476d828a8 100644
--- a/scripts/generators/indexCountryGenerator.ts
+++ b/scripts/generators/indexCountryGenerator.ts
@@ -26,6 +26,13 @@ export class IndexCountryGenerator implements Generator {
.orderBy((stream: Stream) => stream.getTitle())
.filter((stream: Stream) => stream.isSFW())
.forEach((stream: Stream) => {
+ if (stream.isInternational()) {
+ const streamClone = stream.clone()
+ streamClone.groupTitle = 'International'
+ groupedStreams.add(streamClone)
+ return
+ }
+
if (!stream.hasBroadcastArea()) {
const streamClone = stream.clone()
streamClone.groupTitle = 'Undefined'
@@ -41,6 +48,7 @@ export class IndexCountryGenerator implements Generator {
})
groupedStreams = groupedStreams.orderBy((stream: Stream) => {
+ if (stream.groupTitle === 'International') return 'ZZ'
if (stream.groupTitle === 'Undefined') return 'ZZZ'
return stream.groupTitle
diff --git a/scripts/generators/indexRegionGenerator.ts b/scripts/generators/indexRegionGenerator.ts
index 43775ec91e..27a9ae0fe5 100644
--- a/scripts/generators/indexRegionGenerator.ts
+++ b/scripts/generators/indexRegionGenerator.ts
@@ -31,6 +31,8 @@ export class IndexRegionGenerator implements Generator {
if (!stream.hasBroadcastArea()) return
stream.getBroadcastRegions().forEach((region: Region) => {
+ if (region.isWorldwide()) return
+
const streamClone = stream.clone()
streamClone.groupTitle = region.name
groupedStreams.push(streamClone)
diff --git a/scripts/generators/regionsGenerator.ts b/scripts/generators/regionsGenerator.ts
index 02112974ed..60dab3d614 100644
--- a/scripts/generators/regionsGenerator.ts
+++ b/scripts/generators/regionsGenerator.ts
@@ -28,6 +28,8 @@ export class RegionsGenerator implements Generator {
.filter((stream: Stream) => stream.isSFW())
this.regions.forEach(async (region: Region) => {
+ if (region.isWorldwide()) return
+
const regionStreams = streams.filter((stream: Stream) => stream.isBroadcastInRegion(region))
const playlist = new Playlist(regionStreams, { public: true })
diff --git a/scripts/models/broadcastArea.ts b/scripts/models/broadcastArea.ts
index c7392984b1..fa5b43f0e5 100644
--- a/scripts/models/broadcastArea.ts
+++ b/scripts/models/broadcastArea.ts
@@ -31,18 +31,19 @@ export class BroadcastArea {
const city: City = citiesKeyByCode.get(code)
if (!city) return
citiesIncluded.add(city)
- if (city.subdivision) subdivisionsIncluded.add(city.subdivision)
+ regionsIncluded = regionsIncluded.concat(city.getRegions())
}
case 's': {
const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
if (!subdivision) return
- citiesIncluded = citiesIncluded.concat(subdivision.getCities())
subdivisionsIncluded.add(subdivision)
+ regionsIncluded = regionsIncluded.concat(subdivision.getRegions())
}
case 'c': {
const country: Country = countriesKeyByCode.get(code)
if (!country) return
countriesIncluded.add(country)
+ regionsIncluded = regionsIncluded.concat(country.getRegions())
}
case 'r': {
const region: Region = regionsKeyByCode.get(code)
diff --git a/scripts/models/feed.ts b/scripts/models/feed.ts
index 7515fdc652..a6713e265a 100644
--- a/scripts/models/feed.ts
+++ b/scripts/models/feed.ts
@@ -1,4 +1,4 @@
-import { Country, Language, Region, Channel, Subdivision, BroadcastArea } from './index'
+import { Country, Language, Region, Channel, Subdivision, BroadcastArea, City } from './index'
import { Collection, Dictionary } from '@freearhey/core'
import type { FeedData } from '../types/feed'
@@ -120,6 +120,12 @@ export class Feed {
)
}
+ isBroadcastInCity(city: City): boolean {
+ if (!this.broadcastArea) return false
+
+ return this.broadcastArea.includesCity(city)
+ }
+
isBroadcastInSubdivision(subdivision: Subdivision): boolean {
if (!this.broadcastArea) return false
@@ -129,13 +135,19 @@ export class Feed {
isBroadcastInCountry(country: Country): boolean {
if (!this.broadcastArea) return false
- return this.broadcastArea?.includesCountry(country)
+ return this.broadcastArea.includesCountry(country)
}
isBroadcastInRegion(region: Region): boolean {
if (!this.broadcastArea) return false
- return this.broadcastArea?.includesRegion(region)
+ return this.broadcastArea.includesRegion(region)
+ }
+
+ isInternational(): boolean {
+ if (!this.broadcastArea) return false
+
+ return this.broadcastArea.codes.join(',').includes('r/')
}
getGuides(): Collection {
diff --git a/scripts/models/region.ts b/scripts/models/region.ts
index 35e0baa78a..5fe52ad5a9 100644
--- a/scripts/models/region.ts
+++ b/scripts/models/region.ts
@@ -78,6 +78,10 @@ export class Region {
return this.countryCodes.includes((countryCode: string) => countryCode === code)
}
+ isWorldwide(): boolean {
+ return ['INT', 'WW'].includes(this.code)
+ }
+
serialize(): RegionSerializedData {
return {
code: this.code,
diff --git a/scripts/models/stream.ts b/scripts/models/stream.ts
index 395e179207..a465081ea6 100644
--- a/scripts/models/stream.ts
+++ b/scripts/models/stream.ts
@@ -1,4 +1,14 @@
-import { Feed, Channel, Category, Region, Subdivision, Country, Language, Logo } from './index'
+import {
+ Feed,
+ Channel,
+ Category,
+ Region,
+ Subdivision,
+ Country,
+ Language,
+ Logo,
+ City
+} from './index'
import { URL, Collection, Dictionary } from '@freearhey/core'
import type { StreamData } from '../types/stream'
import parser from 'iptv-playlist-parser'
@@ -330,6 +340,10 @@ export class Stream {
return this.feed ? this.feed.broadcastAreaCodes : new Collection()
}
+ isBroadcastInCity(city: City): boolean {
+ return this.feed ? this.feed.isBroadcastInCity(city) : false
+ }
+
isBroadcastInSubdivision(subdivision: Subdivision): boolean {
return this.feed ? this.feed.isBroadcastInSubdivision(subdivision) : false
}
@@ -342,6 +356,10 @@ export class Stream {
return this.feed ? this.feed.isBroadcastInRegion(region) : false
}
+ isInternational(): boolean {
+ return this.feed ? this.feed.isInternational() : false
+ }
+
getLogos(): Collection {
function format(logo: Logo): number {
const levelByFormat = { SVG: 0, PNG: 3, APNG: 1, WebP: 1, AVIF: 1, JPEG: 2, GIF: 1 }
diff --git a/scripts/tables/categoryTable.ts b/scripts/tables/categoriesTable.ts
similarity index 61%
rename from scripts/tables/categoryTable.ts
rename to scripts/tables/categoriesTable.ts
index f82f3ffd4a..0b763e4162 100644
--- a/scripts/tables/categoryTable.ts
+++ b/scripts/tables/categoriesTable.ts
@@ -1,32 +1,35 @@
-import { Storage, Collection, File } from '@freearhey/core'
+import { Storage, Collection, File, Dictionary } from '@freearhey/core'
import { HTMLTable, LogParser, LogItem } from '../core'
+import { LOGS_DIR, README_DIR } from '../constants'
import { Category } from '../models'
-import { DATA_DIR, LOGS_DIR, README_DIR } from '../constants'
import { Table } from './table'
-export class CategoryTable implements Table {
- constructor() {}
+type CategoriesTableProps = {
+ categoriesKeyById: Dictionary
+}
+
+export class CategoriesTable implements Table {
+ categoriesKeyById: Dictionary
+
+ constructor({ categoriesKeyById }: CategoriesTableProps) {
+ this.categoriesKeyById = categoriesKeyById
+ }
async make() {
- const dataStorage = new Storage(DATA_DIR)
- const categoriesContent = await dataStorage.json('categories.json')
- const categories = new Collection(categoriesContent).map(data => new Category(data))
- const categoriesGroupedById = categories.keyBy((category: Category) => category.id)
-
const parser = new LogParser()
const logsStorage = new Storage(LOGS_DIR)
const generatorsLog = await logsStorage.load('generators.log')
- let data = new Collection()
+ let items = new Collection()
parser
.parse(generatorsLog)
.filter((logItem: LogItem) => logItem.type === 'category')
.forEach((logItem: LogItem) => {
const file = new File(logItem.filepath)
const categoryId = file.name()
- const category: Category = categoriesGroupedById.get(categoryId)
+ const category: Category = this.categoriesKeyById.get(categoryId)
- data.add([
+ items.add([
category ? category.name : 'ZZ',
category ? category.name : 'Undefined',
logItem.count,
@@ -34,14 +37,14 @@ export class CategoryTable implements Table {
])
})
- data = data
+ items = items
.orderBy(item => item[0])
.map(item => {
item.shift()
return item
})
- const table = new HTMLTable(data.all(), [
+ const table = new HTMLTable(items.all(), [
{ name: 'Category' },
{ name: 'Channels', align: 'right' },
{ name: 'Playlist', nowrap: true }
diff --git a/scripts/tables/countriesTable.ts b/scripts/tables/countriesTable.ts
new file mode 100644
index 0000000000..0be11c1b3d
--- /dev/null
+++ b/scripts/tables/countriesTable.ts
@@ -0,0 +1,189 @@
+import { Storage, Collection, Dictionary } from '@freearhey/core'
+import { City, Country, Subdivision } from '../models'
+import { LOGS_DIR, README_DIR } from '../constants'
+import { LogParser, LogItem } from '../core'
+import { Table } from './table'
+
+type CountriesTableProps = {
+ countriesKeyByCode: Dictionary
+ subdivisionsKeyByCode: Dictionary
+ countries: Collection
+ subdivisions: Collection
+ cities: Collection
+}
+
+export class CountriesTable implements Table {
+ countriesKeyByCode: Dictionary
+ subdivisionsKeyByCode: Dictionary
+ countries: Collection
+ subdivisions: Collection
+ cities: Collection
+
+ constructor({
+ countriesKeyByCode,
+ subdivisionsKeyByCode,
+ countries,
+ subdivisions,
+ cities
+ }: CountriesTableProps) {
+ this.countriesKeyByCode = countriesKeyByCode
+ this.subdivisionsKeyByCode = subdivisionsKeyByCode
+ this.countries = countries
+ this.subdivisions = subdivisions
+ this.cities = cities
+ }
+
+ async make() {
+ const parser = new LogParser()
+ const logsStorage = new Storage(LOGS_DIR)
+ const generatorsLog = await logsStorage.load('generators.log')
+ const parsed = parser.parse(generatorsLog)
+ const logCountries = parsed.filter((logItem: LogItem) => logItem.type === 'country')
+ const logSubdivisions = parsed.filter((logItem: LogItem) => logItem.type === 'subdivision')
+ const logCities = parsed.filter((logItem: LogItem) => logItem.type === 'city')
+
+ let items = new Collection()
+ this.countries.forEach((country: Country) => {
+ const countriesLogItem = logCountries.find(
+ (logItem: LogItem) => logItem.filepath === `countries/${country.code.toLowerCase()}.m3u`
+ )
+
+ let countryItem = {
+ index: country.name,
+ count: 0,
+ link: `https://iptv-org.github.io/iptv/countries/${country.code.toLowerCase()}.m3u`,
+ name: `${country.flag} ${country.name}`,
+ children: new Collection()
+ }
+
+ if (countriesLogItem) {
+ countryItem.count = countriesLogItem.count
+ }
+
+ const countrySubdivisions = this.subdivisions.filter(
+ (subdivision: Subdivision) => subdivision.countryCode === country.code
+ )
+ const countryCities = this.cities.filter((city: City) => city.countryCode === country.code)
+ if (countrySubdivisions.notEmpty()) {
+ this.subdivisions.forEach((subdivision: Subdivision) => {
+ if (subdivision.countryCode !== country.code) return
+ const subdivisionCities = countryCities.filter(
+ (city: City) =>
+ (city.subdivisionCode && city.subdivisionCode === subdivision.code) ||
+ city.countryCode === subdivision.countryCode
+ )
+ const subdivisionsLogItem = logSubdivisions.find(
+ (logItem: LogItem) =>
+ logItem.filepath === `subdivisions/${subdivision.code.toLowerCase()}.m3u`
+ )
+
+ let subdivisionItem = {
+ index: subdivision.name,
+ name: subdivision.name,
+ count: 0,
+ link: `https://iptv-org.github.io/iptv/subdivisions/${subdivision.code.toLowerCase()}.m3u`,
+ children: new Collection()
+ }
+
+ if (subdivisionsLogItem) {
+ subdivisionItem.count = subdivisionsLogItem.count
+ }
+
+ subdivisionCities.forEach((city: City) => {
+ if (city.countryCode !== country.code || city.subdivisionCode !== subdivision.code)
+ return
+ const citiesLogItem = logCities.find(
+ (logItem: LogItem) => logItem.filepath === `cities/${city.code.toLowerCase()}.m3u`
+ )
+
+ if (!citiesLogItem) return
+
+ subdivisionItem.children.add({
+ index: city.name,
+ name: city.name,
+ count: citiesLogItem.count,
+ link: `https://iptv-org.github.io/iptv/${citiesLogItem.filepath}`
+ })
+ })
+
+ if (subdivisionItem.count > 0 || subdivisionItem.children.notEmpty()) {
+ countryItem.children.add(subdivisionItem)
+ }
+ })
+ } else if (countryCities.notEmpty()) {
+ countryCities.forEach((city: City) => {
+ const citiesLogItem = logCities.find(
+ (logItem: LogItem) => logItem.filepath === `cities/${city.code.toLowerCase()}.m3u`
+ )
+
+ if (!citiesLogItem) return
+
+ countryItem.children.add({
+ index: city.name,
+ name: city.name,
+ count: citiesLogItem.count,
+ link: `https://iptv-org.github.io/iptv/${citiesLogItem.filepath}`,
+ children: new Collection()
+ })
+ })
+ }
+
+ if (countryItem.count > 0 || countryItem.children.notEmpty()) {
+ items.add(countryItem)
+ }
+ })
+
+ const internationalLogItem = logCountries.find(
+ (logItem: LogItem) => logItem.filepath === 'countries/int.m3u'
+ )
+
+ if (internationalLogItem) {
+ items.push({
+ index: 'ZZ',
+ name: '🌐 International',
+ count: internationalLogItem.count,
+ link: `https://iptv-org.github.io/iptv/${internationalLogItem.filepath}`,
+ children: new Collection()
+ })
+ }
+
+ const undefinedLogItem = logCountries.find(
+ (logItem: LogItem) => logItem.filepath === `countries/undefined.m3u`
+ )
+
+ if (undefinedLogItem) {
+ items.push({
+ index: 'ZZZ',
+ name: 'Undefined',
+ count: undefinedLogItem.count,
+ link: `https://iptv-org.github.io/iptv/${undefinedLogItem.filepath}`,
+ children: new Collection()
+ })
+ }
+
+ items = items.orderBy(item => item.index)
+
+ const output = items
+ .map(item => {
+ let row = `- ${item.name} ${item.link}`
+
+ item.children
+ .orderBy(item => item.index)
+ .forEach(item => {
+ row += `\r\n\ - ${item.name} ${item.link}`
+
+ item.children
+ .orderBy(item => item.index)
+ .forEach(item => {
+ row += `\r\n\ - ${item.name} ${item.link}`
+ })
+ })
+
+ return row
+ })
+ .join('\r\n')
+
+ const readmeStorage = new Storage(README_DIR)
+ await readmeStorage.save('_countries.md', output)
+ }
+}
diff --git a/scripts/tables/countryTable.ts b/scripts/tables/countryTable.ts
deleted file mode 100644
index d0075e62cd..0000000000
--- a/scripts/tables/countryTable.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Storage, Collection, File } from '@freearhey/core'
-import { HTMLTable, LogParser, LogItem } from '../core'
-import { Country } from '../models'
-import { DATA_DIR, LOGS_DIR, README_DIR } from '../constants'
-import { Table } from './table'
-
-export class CountryTable implements Table {
- constructor() {}
-
- async make() {
- const dataStorage = new Storage(DATA_DIR)
-
- const countriesContent = await dataStorage.json('countries.json')
- const countries = new Collection(countriesContent).map(data => new Country(data))
- const countriesGroupedByCode = countries.keyBy((country: Country) => country.code)
-
- const parser = new LogParser()
- const logsStorage = new Storage(LOGS_DIR)
- const generatorsLog = await logsStorage.load('generators.log')
- const parsed = parser.parse(generatorsLog)
-
- let data = new Collection()
-
- parsed
- .filter((logItem: LogItem) => logItem.type === 'country')
- .forEach((logItem: LogItem) => {
- const file = new File(logItem.filepath)
- const code = file.name().toUpperCase()
- const [countryCode] = code.split('-') || ['', '']
- const country = countriesGroupedByCode.get(countryCode)
-
- if (country) {
- data.add([
- country.name,
- `${country.flag} ${country.name}`,
- logItem.count,
- `https://iptv-org.github.io/iptv/${logItem.filepath}`
- ])
- } else {
- data.add([
- 'ZZ',
- 'Undefined',
- logItem.count,
- `https://iptv-org.github.io/iptv/${logItem.filepath}`
- ])
- }
- })
-
- data = data
- .orderBy(item => item[0])
- .map(item => {
- item.shift()
- return item
- })
-
- const table = new HTMLTable(data.all(), [
- { name: 'Country' },
- { name: 'Channels', align: 'right' },
- { name: 'Playlist', nowrap: true }
- ])
-
- const readmeStorage = new Storage(README_DIR)
- await readmeStorage.save('_countries.md', table.toString())
- }
-}
diff --git a/scripts/tables/index.ts b/scripts/tables/index.ts
index 6da33e8221..f25c0a3a27 100644
--- a/scripts/tables/index.ts
+++ b/scripts/tables/index.ts
@@ -1,5 +1,4 @@
-export * from './categoryTable'
-export * from './countryTable'
-export * from './languageTable'
-export * from './regionTable'
-export * from './subdivisionTable'
+export * from './categoriesTable'
+export * from './countriesTable'
+export * from './languagesTable'
+export * from './regionsTable'
diff --git a/scripts/tables/languageTable.ts b/scripts/tables/languagesTable.ts
similarity index 68%
rename from scripts/tables/languageTable.ts
rename to scripts/tables/languagesTable.ts
index 2014ba6760..7621907453 100644
--- a/scripts/tables/languageTable.ts
+++ b/scripts/tables/languagesTable.ts
@@ -1,18 +1,21 @@
-import { Storage, Collection, File } from '@freearhey/core'
+import { Storage, Collection, File, Dictionary } from '@freearhey/core'
import { HTMLTable, LogParser, LogItem } from '../core'
+import { LOGS_DIR, README_DIR } from '../constants'
import { Language } from '../models'
-import { DATA_DIR, LOGS_DIR, README_DIR } from '../constants'
import { Table } from './table'
-export class LanguageTable implements Table {
- constructor() {}
+type LanguagesTableProps = {
+ languagesKeyByCode: Dictionary
+}
+
+export class LanguagesTable implements Table {
+ languagesKeyByCode: Dictionary
+
+ constructor({ languagesKeyByCode }: LanguagesTableProps) {
+ this.languagesKeyByCode = languagesKeyByCode
+ }
async make() {
- const dataStorage = new Storage(DATA_DIR)
- const languagesContent = await dataStorage.json('languages.json')
- const languages = new Collection(languagesContent).map(data => new Language(data))
- const languagesGroupedByCode = languages.keyBy((language: Language) => language.code)
-
const parser = new LogParser()
const logsStorage = new Storage(LOGS_DIR)
const generatorsLog = await logsStorage.load('generators.log')
@@ -24,7 +27,7 @@ export class LanguageTable implements Table {
.forEach((logItem: LogItem) => {
const file = new File(logItem.filepath)
const languageCode = file.name()
- const language: Language = languagesGroupedByCode.get(languageCode)
+ const language: Language = this.languagesKeyByCode.get(languageCode)
data.add([
language ? language.name : 'ZZ',
diff --git a/scripts/tables/regionTable.ts b/scripts/tables/regionTable.ts
deleted file mode 100644
index 84eeaaa4a2..0000000000
--- a/scripts/tables/regionTable.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Storage, Collection, File } from '@freearhey/core'
-import { HTMLTable, LogParser, LogItem } from '../core'
-import { Region } from '../models'
-import { DATA_DIR, LOGS_DIR, README_DIR } from '../constants'
-import { Table } from './table'
-
-export class RegionTable implements Table {
- constructor() {}
-
- async make() {
- const dataStorage = new Storage(DATA_DIR)
- const regionsContent = await dataStorage.json('regions.json')
- const regions = new Collection(regionsContent).map(data => new Region(data))
- const regionsGroupedByCode = regions.keyBy((region: Region) => region.code)
-
- const parser = new LogParser()
- const logsStorage = new Storage(LOGS_DIR)
- const generatorsLog = await logsStorage.load('generators.log')
-
- let data = new Collection()
- parser
- .parse(generatorsLog)
- .filter((logItem: LogItem) => logItem.type === 'region')
- .forEach((logItem: LogItem) => {
- const file = new File(logItem.filepath)
- const regionCode = file.name().toUpperCase()
- const region: Region = regionsGroupedByCode.get(regionCode)
-
- if (region) {
- data.add([
- region.name,
- region.name,
- logItem.count,
- `https://iptv-org.github.io/iptv/${logItem.filepath}`
- ])
- } else {
- data.add([
- 'ZZZ',
- 'Undefined',
- logItem.count,
- `https://iptv-org.github.io/iptv/${logItem.filepath}`
- ])
- }
- })
-
- data = data
- .orderBy(item => item[0])
- .map(item => {
- item.shift()
- return item
- })
-
- const table = new HTMLTable(data.all(), [
- { name: 'Region', align: 'left' },
- { name: 'Channels', align: 'right' },
- { name: 'Playlist', align: 'left', nowrap: true }
- ])
-
- const readmeStorage = new Storage(README_DIR)
- await readmeStorage.save('_regions.md', table.toString())
- }
-}
diff --git a/scripts/tables/regionsTable.ts b/scripts/tables/regionsTable.ts
new file mode 100644
index 0000000000..25f2e71bcb
--- /dev/null
+++ b/scripts/tables/regionsTable.ts
@@ -0,0 +1,52 @@
+import { Storage, Collection, File } from '@freearhey/core'
+import { HTMLTable, LogParser, LogItem } from '../core'
+import { LOGS_DIR, README_DIR } from '../constants'
+import { Region } from '../models'
+import { Table } from './table'
+
+type RegionsTableProps = {
+ regions: Collection
+}
+
+export class RegionsTable implements Table {
+ regions: Collection
+
+ constructor({ regions }: RegionsTableProps) {
+ this.regions = regions
+ }
+
+ async make() {
+ const parser = new LogParser()
+ const logsStorage = new Storage(LOGS_DIR)
+ const generatorsLog = await logsStorage.load('generators.log')
+ const parsed = parser.parse(generatorsLog)
+ const logRegions = parsed.filter((logItem: LogItem) => logItem.type === 'region')
+
+ let items = new Collection()
+ this.regions.forEach((region: Region) => {
+ const logItem = logRegions.find(
+ (logItem: LogItem) => logItem.filepath === `regions/${region.code.toLowerCase()}.m3u`
+ )
+
+ if (!logItem) return
+
+ items.add({
+ index: region.name,
+ name: region.name,
+ count: logItem.count,
+ link: `https://iptv-org.github.io/iptv/${logItem.filepath}`
+ })
+ })
+
+ items = items.orderBy(item => item.index)
+
+ const output = items
+ .map(item => {
+ return `- ${item.name} ${item.link}`
+ })
+ .join('\r\n')
+
+ const readmeStorage = new Storage(README_DIR)
+ await readmeStorage.save('_regions.md', output)
+ }
+}
diff --git a/scripts/tables/subdivisionTable.ts b/scripts/tables/subdivisionTable.ts
deleted file mode 100644
index 925d9094e3..0000000000
--- a/scripts/tables/subdivisionTable.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { Storage, Collection, File } from '@freearhey/core'
-import { HTMLTable, LogParser, LogItem } from '../core'
-import { Country, Subdivision } from '../models'
-import { DATA_DIR, LOGS_DIR, README_DIR } from '../constants'
-import { Table } from './table'
-
-export class SubdivisionTable implements Table {
- constructor() {}
-
- async make() {
- const dataStorage = new Storage(DATA_DIR)
-
- const countriesContent = await dataStorage.json('countries.json')
- const countries = new Collection(countriesContent).map(data => new Country(data))
- const countriesGroupedByCode = countries.keyBy((country: Country) => country.code)
- const subdivisionsContent = await dataStorage.json('subdivisions.json')
- const subdivisions = new Collection(subdivisionsContent).map(data => new Subdivision(data))
- const subdivisionsGroupedByCode = subdivisions.keyBy(
- (subdivision: Subdivision) => subdivision.code
- )
-
- const parser = new LogParser()
- const logsStorage = new Storage(LOGS_DIR)
- const generatorsLog = await logsStorage.load('generators.log')
- const parsed = parser.parse(generatorsLog)
- const parsedSubdivisions = parsed.filter((logItem: LogItem) => logItem.type === 'subdivision')
-
- let output = ''
- countries.forEach((country: Country) => {
- const parsedCountrySubdivisions = parsedSubdivisions.filter((logItem: LogItem) =>
- logItem.filepath.includes(`subdivisions/${country.code.toLowerCase()}`)
- )
-
- if (!parsedCountrySubdivisions.length) return
-
- output += `\r\n\r\n${country.name}
\r\n`
-
- const data = new Collection()
-
- parsedCountrySubdivisions.forEach((logItem: LogItem) => {
- const file = new File(logItem.filepath)
- const code = file.name().toUpperCase()
- const [countryCode, subdivisionCode] = code.split('-') || ['', '']
- const country = countriesGroupedByCode.get(countryCode)
-
- if (country && subdivisionCode) {
- const subdivision = subdivisionsGroupedByCode.get(code)
- if (subdivision) {
- data.add([
- subdivision.name,
- logItem.count,
- `https://iptv-org.github.io/iptv/${logItem.filepath}`
- ])
- }
- }
- })
-
- const table = new HTMLTable(data.all(), [
- { name: 'Subdivision' },
- { name: 'Channels', align: 'right' },
- { name: 'Playlist', nowrap: true }
- ])
-
- output += table.toString()
-
- output += '\r\n '
- })
-
- const readmeStorage = new Storage(README_DIR)
- await readmeStorage.save('_subdivisions.md', output.trim())
- }
-}