mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
41 files changed
+1452
-657
No files matched your search
Generated
+20
-52
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "iptvnator",
|
||||
"version": "1.0.0-beta.2",
|
||||
"version": "1.0.0-beta.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "iptvnator",
|
||||
"version": "1.0.0-beta.2",
|
||||
"version": "1.0.0-beta.3",
|
||||
"dependencies": {
|
||||
"@angular-builders/jest": "17.0.0",
|
||||
"@angular/animations": "18.2.1",
|
||||
@@ -16,6 +16,7 @@
|
||||
"@ngrx/effects": "18.0.2",
|
||||
"@ngrx/entity": "18.0.2",
|
||||
"@ngrx/router-store": "18.0.2",
|
||||
"@ngrx/signals": "18.1.1",
|
||||
"@ngrx/store": "18.0.2",
|
||||
"@ngrx/store-devtools": "18.0.2",
|
||||
"@ngx-pwa/local-storage": "18.0.0",
|
||||
@@ -39,7 +40,6 @@
|
||||
"iptv-playlist-parser": "github:4gray/iptv-playlist-parser",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.30.1",
|
||||
"nedb-promises": "6.0.3",
|
||||
"ngx-indexed-db": "19.0.0",
|
||||
"ngx-skeleton-loader": "9.0.0",
|
||||
"ngx-whats-new": "0.4.0",
|
||||
@@ -5656,6 +5656,23 @@
|
||||
"rxjs": "^6.5.3 || ^7.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrx/signals": {
|
||||
"version": "18.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-18.1.1.tgz",
|
||||
"integrity": "sha512-7AaFp4VrzYhnDHlF15VZAMvp067zIC5arRRRMV+i+oi95sQVDXezpj74KTIguwRbZ3FU+GKGzFCNrwgYvginSg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "^18.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rxjs": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrx/store": {
|
||||
"version": "18.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@ngrx/store/-/store-18.0.2.tgz",
|
||||
@@ -6615,26 +6632,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@seald-io/binary-search-tree": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz",
|
||||
"integrity": "sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA=="
|
||||
},
|
||||
"node_modules/@seald-io/nedb": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-2.2.2.tgz",
|
||||
"integrity": "sha512-z91J3pem4ENzHuu9BilOSdlGL2S14OQYePPdvBcPHgHr+s51VIUwQARcOjx21KvtkA27vEpgPqzrVKh7nSlIfw==",
|
||||
"dependencies": {
|
||||
"@seald-io/binary-search-tree": "^1.0.2",
|
||||
"async": "0.2.10",
|
||||
"localforage": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@seald-io/nedb/node_modules/async": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
@@ -14843,11 +14840,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
|
||||
@@ -17897,14 +17889,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@@ -18143,14 +18127,6 @@
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/localforage": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||
"dependencies": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -19290,14 +19266,6 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nedb-promises": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nedb-promises/-/nedb-promises-6.0.3.tgz",
|
||||
"integrity": "sha512-pd0N6EsIPhXsfqdLcBJGig1FoTdV8wtuiXB7rBUqycJdUSA6wdI5BlkwEAjDBWIwZc4+xqBHRLtuAB7/bjS+rg==",
|
||||
"dependencies": {
|
||||
"@seald-io/nedb": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/needle": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "iptvnator",
|
||||
"version": "1.0.0-beta.2",
|
||||
"version": "1.0.0-beta.3",
|
||||
"description": "IPTV player application.",
|
||||
"homepage": "https://github.com/4gray/iptvnator",
|
||||
"author": {
|
||||
@@ -43,6 +43,7 @@
|
||||
"@ngrx/effects": "18.0.2",
|
||||
"@ngrx/entity": "18.0.2",
|
||||
"@ngrx/router-store": "18.0.2",
|
||||
"@ngrx/signals": "18.1.1",
|
||||
"@ngrx/store": "18.0.2",
|
||||
"@ngrx/store-devtools": "18.0.2",
|
||||
"@ngx-pwa/local-storage": "18.0.0",
|
||||
|
||||
Generated
+373
-36
@@ -4,10 +4,14 @@ version = 3
|
||||
|
||||
[[package]]
|
||||
name = "IPTVnator"
|
||||
version = "1.0.0-beta.2"
|
||||
version = "1.0.0-beta.3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"fix-path-env",
|
||||
"flate2",
|
||||
"log",
|
||||
"quick-xml 0.31.0",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -21,6 +25,8 @@ dependencies = [
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
"xmltv",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -159,6 +165,19 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
@@ -528,8 +547,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
@@ -544,7 +565,7 @@ dependencies = [
|
||||
"cocoa-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
@@ -651,7 +672,7 @@ dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -954,7 +975,7 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"toml 0.8.2",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1137,6 +1158,15 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1144,7 +1174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1158,6 +1188,12 @@ dependencies = [
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -1565,6 +1601,25 @@ dependencies = [
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap 2.6.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.6"
|
||||
@@ -1576,7 +1631,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"indexmap 2.6.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
@@ -1646,6 +1701,17 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.1.0"
|
||||
@@ -1657,6 +1723,17 @@ dependencies = [
|
||||
"itoa 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
@@ -1664,7 +1741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1675,8 +1752,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -1686,6 +1763,36 @@ version = "1.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.11",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.5.0"
|
||||
@@ -1695,9 +1802,9 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"h2 0.4.6",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"itoa 1.0.11",
|
||||
"pin-project-lite",
|
||||
@@ -1713,8 +1820,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"http 1.1.0",
|
||||
"hyper 1.5.0",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
@@ -1724,6 +1831,19 @@ dependencies = [
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper 0.14.31",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.10"
|
||||
@@ -1733,9 +1853,9 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.5.0",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
@@ -2188,6 +2308,23 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -2273,7 +2410,7 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
@@ -2542,6 +2679,50 @@ dependencies = [
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -2947,6 +3128,16 @@ dependencies = [
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.32.0"
|
||||
@@ -3174,6 +3365,48 @@ dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.31",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 0.1.2",
|
||||
"system-configuration 0.5.1",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.9"
|
||||
@@ -3187,11 +3420,11 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"h2 0.4.6",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper 1.5.0",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
@@ -3203,13 +3436,13 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"sync_wrapper 1.0.1",
|
||||
"system-configuration 0.6.1",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
@@ -3354,6 +3587,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
@@ -3395,6 +3637,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.21"
|
||||
@@ -3440,6 +3691,29 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.22.0"
|
||||
@@ -3705,7 +3979,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases 0.2.1",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
@@ -3854,6 +4128,12 @@ dependencies = [
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.1"
|
||||
@@ -3872,6 +4152,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
@@ -3880,7 +4171,17 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
"system-configuration-sys 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3995,7 +4296,7 @@ dependencies = [
|
||||
"glob",
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -4007,7 +4308,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"reqwest 0.12.9",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -4155,9 +4456,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c752aee1b00ec3c4d4f440095995d9bd2c640b478f2067d1fba388900b82eb96"
|
||||
dependencies = [
|
||||
"data-url",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest 0.12.9",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4251,11 +4552,11 @@ dependencies = [
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"infer",
|
||||
"minisign-verify",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"reqwest 0.12.9",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4294,7 +4595,7 @@ checksum = "a1ef7363e7229ac8d04e8a5d405670dbd43dde8fc4bc3bc56105c35452d03784"
|
||||
dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"jni",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
@@ -4312,7 +4613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62fa2068e8498ad007b54d5773d03d57c3ff6dd96f8c8ce58beff44d0d5e0d30"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"jni",
|
||||
"log",
|
||||
"objc2",
|
||||
@@ -4485,6 +4786,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
@@ -4504,6 +4806,16 @@ dependencies = [
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
@@ -4817,6 +5129,12 @@ version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
@@ -5509,6 +5827,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
@@ -5533,7 +5861,7 @@ dependencies = [
|
||||
"gdkx11",
|
||||
"gtk",
|
||||
"html5ever",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"kuchikiki",
|
||||
@@ -5611,6 +5939,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xmltv"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe9c95973cc08e1ebbc63705f61643d63a72614dd42d4c33f0e789da5c96b3d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.1.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "IPTVnator"
|
||||
version = "1.0.0-beta.2"
|
||||
version = "1.0.0-beta.3"
|
||||
description = "IPTVnator"
|
||||
authors = ["4gray"]
|
||||
license = "MIT"
|
||||
@@ -21,7 +21,7 @@ tauri-build = { version = "2.0.1", features = [] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.0.6", features = [] }
|
||||
tauri = { version = "2.0.6", features = ["macos-private-api"] }
|
||||
tauri-plugin-log = "2.0.0-rc"
|
||||
tauri-plugin-http = {version = "2", features = ["unsafe-headers"] }
|
||||
tauri-plugin-shell = "2"
|
||||
@@ -30,6 +30,12 @@ tauri-plugin-os = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
xmltv = "0.9"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
reqwest = { version = "0.11", features = ["json", "gzip"] }
|
||||
flate2 = "1.0"
|
||||
quick-xml = { version = "0.31", features = ["serialize"] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"fs:default",
|
||||
"updater:default",
|
||||
"dialog:default",
|
||||
"process:default"
|
||||
"process:default",
|
||||
"core:window:allow-set-fullscreen"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use flate2::read::GzDecoder;
|
||||
use log::{info, warn};
|
||||
use quick_xml::de::from_str;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::sync::Mutex;
|
||||
use tokio::sync::OnceCell;
|
||||
use xmltv::Tv;
|
||||
|
||||
// Modified to store multiple EPG sources
|
||||
#[derive(Debug)]
|
||||
struct EpgStore {
|
||||
sources: Vec<EpgSource>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EpgSource {
|
||||
url: String,
|
||||
programs: HashMap<String, Vec<Program>>,
|
||||
}
|
||||
|
||||
// Replace the static EPG_DATA with the new structure
|
||||
static EPG_DATA: OnceCell<Mutex<Option<EpgStore>>> = OnceCell::const_new();
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Program {
|
||||
pub start: DateTime<Utc>,
|
||||
pub stop: DateTime<Utc>,
|
||||
pub title: String,
|
||||
pub desc: Option<String>,
|
||||
pub channel: String,
|
||||
pub category: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_xmltv_datetime(datetime: &str) -> Option<DateTime<Utc>> {
|
||||
DateTime::parse_from_str(datetime, "%Y%m%d%H%M%S %z")
|
||||
.ok()
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
}
|
||||
|
||||
pub async fn fetch_and_parse_epg(url: String) -> Result<(), Box<dyn std::error::Error>> {
|
||||
info!("Starting EPG fetch from URL: {}", url);
|
||||
|
||||
let new_source = fetch_single_epg_source(url.clone()).await?;
|
||||
|
||||
// Update EPG store
|
||||
match EPG_DATA.get() {
|
||||
Some(epg_data) => {
|
||||
let mut guard = epg_data.lock().unwrap();
|
||||
let store = guard.get_or_insert_with(|| EpgStore {
|
||||
sources: Vec::new(),
|
||||
});
|
||||
|
||||
// Remove existing source with same URL if exists
|
||||
store.sources.retain(|source| source.url != url);
|
||||
// Add new source
|
||||
store.sources.push(new_source);
|
||||
info!("Updated EPG store for URL: {}", url);
|
||||
}
|
||||
None => {
|
||||
EPG_DATA
|
||||
.set(Mutex::new(Some(EpgStore {
|
||||
sources: vec![new_source],
|
||||
})))
|
||||
.unwrap();
|
||||
info!("Created new EPG data store");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// New helper function to fetch and parse a single EPG source
|
||||
async fn fetch_single_epg_source(url: String) -> Result<EpgSource, Box<dyn std::error::Error>> {
|
||||
info!("Starting EPG fetch from URL: {}", url);
|
||||
|
||||
// Fetch content
|
||||
let client = reqwest::Client::new();
|
||||
let content = if url.ends_with(".gz") {
|
||||
info!("Fetching and decompressing gzipped EPG data...");
|
||||
let bytes = client.get(&url).send().await?.bytes().await?;
|
||||
let mut decoder = GzDecoder::new(&bytes[..]);
|
||||
let mut string = String::new();
|
||||
decoder.read_to_string(&mut string)?;
|
||||
info!("Successfully decompressed EPG data");
|
||||
string
|
||||
} else {
|
||||
info!("Fetching EPG data...");
|
||||
client.get(&url).send().await?.text().await?
|
||||
};
|
||||
|
||||
info!("Parsing EPG XML data...");
|
||||
let tv: Tv = from_str(&content)?;
|
||||
|
||||
// Get channel name mapping
|
||||
let channel_names: HashMap<String, String> = tv
|
||||
.channels
|
||||
.into_iter()
|
||||
.filter_map(|channel| {
|
||||
channel
|
||||
.display_names
|
||||
.first()
|
||||
.map(|name| (channel.id, name.name.clone()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Converting EPG programs...");
|
||||
let programs: Vec<Program> = tv
|
||||
.programmes
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
let channel_name = match channel_names.get(&p.channel) {
|
||||
Some(name) => name.clone(),
|
||||
None => {
|
||||
warn!("No channel name found for id: {}", p.channel);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let parse_datetime = |opt_str: &Option<String>| -> Option<DateTime<Utc>> {
|
||||
opt_str.as_deref().and_then(parse_xmltv_datetime)
|
||||
};
|
||||
|
||||
let start = match parse_datetime(&Some(p.start)) {
|
||||
Some(dt) => dt,
|
||||
None => {
|
||||
warn!(
|
||||
"Invalid start time format for program in channel {}",
|
||||
p.channel
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let stop = match parse_datetime(&p.stop) {
|
||||
Some(dt) => dt,
|
||||
None => {
|
||||
warn!(
|
||||
"Invalid stop time format for program in channel {}",
|
||||
p.channel
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let title = match p.titles.first() {
|
||||
Some(t) => t.value.clone(),
|
||||
None => {
|
||||
warn!("Program without title found in channel {}", p.channel);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(Program {
|
||||
start,
|
||||
stop,
|
||||
title,
|
||||
desc: p.descriptions.first().map(|d| d.value.clone()),
|
||||
channel: channel_name,
|
||||
category: p.categories.first().map(|c| c.name.clone()),
|
||||
icon: p.icons.first().map(|i| i.src.clone()),
|
||||
language: p.language.map(|lang| lang.value),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Successfully parsed {} EPG programs", programs.len());
|
||||
|
||||
// Group by channel
|
||||
let program_map = programs
|
||||
.into_iter()
|
||||
.fold(HashMap::new(), |mut acc, program| {
|
||||
acc.entry(program.channel.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(program);
|
||||
acc
|
||||
});
|
||||
|
||||
info!("EPG data grouped into {} channels", program_map.len());
|
||||
|
||||
Ok(EpgSource {
|
||||
url,
|
||||
programs: program_map,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_programs_by_channel(
|
||||
channel_name: String,
|
||||
) -> Result<Vec<Program>, Box<dyn std::error::Error>> {
|
||||
info!("Retrieving programs for channel name: {}", channel_name);
|
||||
|
||||
let programs = match EPG_DATA.get() {
|
||||
Some(data) => {
|
||||
let guard = data.lock().unwrap();
|
||||
match &*guard {
|
||||
Some(store) => {
|
||||
// Search through all sources and combine matches
|
||||
let mut all_programs = Vec::new();
|
||||
for source in &store.sources {
|
||||
if let Some(programs) = source.programs.get(&channel_name) {
|
||||
info!(
|
||||
"Found {} programs for channel {} in source {}",
|
||||
programs.len(),
|
||||
channel_name,
|
||||
source.url
|
||||
);
|
||||
all_programs.extend(programs.clone());
|
||||
}
|
||||
}
|
||||
// Sort programs by start time
|
||||
all_programs.sort_by(|a, b| a.start.cmp(&b.start));
|
||||
all_programs
|
||||
}
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
if programs.is_empty() {
|
||||
warn!("No programs found for channel name: {}", channel_name);
|
||||
} else {
|
||||
info!(
|
||||
"Found {} total programs for channel {}",
|
||||
programs.len(),
|
||||
channel_name
|
||||
);
|
||||
}
|
||||
|
||||
Ok(programs)
|
||||
}
|
||||
+26
-1
@@ -78,6 +78,26 @@ async fn open_in_vlc(url: String, path: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod epg;
|
||||
use epg::get_programs_by_channel;
|
||||
|
||||
#[tauri::command]
|
||||
async fn fetch_epg(url: Vec<String>) -> Result<(), String> {
|
||||
for single_url in url {
|
||||
epg::fetch_and_parse_epg(single_url)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_channel_programs(channel_id: String) -> Result<Vec<epg::Program>, String> {
|
||||
get_programs_by_channel(channel_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -89,7 +109,12 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_window_state::Builder::new().build())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.invoke_handler(tauri::generate_handler![open_in_mpv, open_in_vlc])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
fetch_epg,
|
||||
get_channel_programs,
|
||||
open_in_mpv,
|
||||
open_in_vlc
|
||||
])
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "iptvnator",
|
||||
"version": "1.0.0-2",
|
||||
"version": "1.0.0-3",
|
||||
"identifier": "com.4gray.dev",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
@@ -28,6 +28,7 @@
|
||||
"titleBarStyle": "Transparent"
|
||||
}
|
||||
],
|
||||
"macOSPrivateApi": true,
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": [
|
||||
|
||||
@@ -12,8 +12,6 @@ import * as semver from 'semver';
|
||||
import { IpcCommand } from '../../shared/ipc-command.class';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
EPG_ERROR,
|
||||
EPG_FETCH_DONE,
|
||||
ERROR,
|
||||
OPEN_FILE,
|
||||
SETTINGS_UPDATE,
|
||||
@@ -50,8 +48,6 @@ export class AppComponent {
|
||||
commandsList = [
|
||||
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
|
||||
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
|
||||
new IpcCommand(EPG_FETCH_DONE, () => this.epgService.onEpgFetchDone()),
|
||||
new IpcCommand(EPG_ERROR, () => this.epgService.onEpgError()),
|
||||
new IpcCommand(SHOW_WHATS_NEW, () => this.showWhatsNewDialog()),
|
||||
new IpcCommand(ERROR, (response: { message: string; status: number }) =>
|
||||
this.showErrorAsNotification(response)
|
||||
|
||||
@@ -48,17 +48,14 @@ function isTauri() {
|
||||
/**
|
||||
* Conditionally imports the necessary service based on the current environment
|
||||
*/
|
||||
export function DataFactory(
|
||||
ngxIndexedDBService: NgxIndexedDBService,
|
||||
http: HttpClient
|
||||
) {
|
||||
export function DataFactory() {
|
||||
if (isElectron()) {
|
||||
return new ElectronService();
|
||||
}
|
||||
if (isTauri()) {
|
||||
return new TauriService();
|
||||
}
|
||||
return new PwaService(http);
|
||||
return new PwaService();
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
|
||||
+95
-88
@@ -13,6 +13,8 @@
|
||||
matInput
|
||||
class="full-width"
|
||||
type="search"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
#search
|
||||
[placeholder]="'CHANNELS.SEARCH_CHANNEL' | translate"
|
||||
[(ngModel)]="searchTerm.name"
|
||||
@@ -55,100 +57,105 @@
|
||||
</ng-template>
|
||||
<mat-nav-list id="groups-list">
|
||||
<mat-accordion multi>
|
||||
<ng-container
|
||||
*ngFor="let groups of groupedChannels | keyvalue"
|
||||
>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header
|
||||
*ngIf="groups.value.length > 0"
|
||||
>
|
||||
{{
|
||||
groups.key ||
|
||||
('CHANNELS.UNGROUPED' | translate)
|
||||
| titlecase
|
||||
}}
|
||||
({{ groups.value.length }})
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<ng-container
|
||||
*ngFor="
|
||||
let channel of groups.value;
|
||||
index as i;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
>
|
||||
<app-channel-list-item
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name ||
|
||||
'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
@for (groups of groupedChannels | keyvalue; track $index) {
|
||||
@if (groups.value.length > 0) {
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
{{
|
||||
groups.key ||
|
||||
('CHANNELS.UNGROUPED' | translate)
|
||||
| titlecase
|
||||
}}
|
||||
({{ groups.value.length }})
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<ng-template matExpansionPanelContent>
|
||||
<ng-container
|
||||
*ngFor="
|
||||
let channel of groups.value;
|
||||
index as i;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="selected?.id === channel.id"
|
||||
></app-channel-list-item>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</ng-container>
|
||||
>
|
||||
<app-channel-list-item
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name ||
|
||||
'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="
|
||||
selected?.id === channel.id
|
||||
"
|
||||
></app-channel-list-item>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
}
|
||||
</mat-accordion>
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab *ngIf="(playlistId$ | async) !== 'GLOBAL_FAVORITES'">
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">star</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'CHANNELS.FAVORITES' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<mat-nav-list
|
||||
cdkDropList
|
||||
*ngIf="favorites$ | async as favorites"
|
||||
(cdkDropListDropped)="drop($event, favorites)"
|
||||
id="favorites-list"
|
||||
>
|
||||
<ng-container *ngIf="favorites.length > 0; else noFavorites">
|
||||
<app-channel-list-item
|
||||
*ngFor="
|
||||
let channel of favorites;
|
||||
index as i;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name || 'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
"
|
||||
[isDraggable]="true"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="selected?.id === channel?.id"
|
||||
[showFavoriteButton]="true"
|
||||
(favoriteToggled)="
|
||||
toggleFavoriteChannel(channel, $event)
|
||||
"
|
||||
></app-channel-list-item>
|
||||
</ng-container>
|
||||
<ng-template #noFavorites>
|
||||
<mat-list-item
|
||||
><strong>{{
|
||||
'CHANNELS.NO_FAVORITES' | translate
|
||||
}}</strong></mat-list-item
|
||||
>
|
||||
<mat-list-item>
|
||||
{{
|
||||
'CHANNELS.USE_STAR_TO_FAVORITE' | translate
|
||||
}}</mat-list-item
|
||||
>
|
||||
@let showFavorites = (playlistId$ | async) !== 'GLOBAL_FAVORITES';
|
||||
@if (showFavorites) {
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">star</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'CHANNELS.FAVORITES' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
@let favorites = favorites$ | async;
|
||||
@if (favorites) {
|
||||
<mat-nav-list
|
||||
cdkDropList
|
||||
(cdkDropListDropped)="drop($event, favorites)"
|
||||
id="favorites-list"
|
||||
>
|
||||
@if (favorites.length > 0) {
|
||||
<app-channel-list-item
|
||||
*ngFor="
|
||||
let channel of favorites;
|
||||
index as i;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name || 'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
"
|
||||
[isDraggable]="true"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="selected?.id === channel?.id"
|
||||
[showFavoriteButton]="true"
|
||||
(favoriteToggled)="
|
||||
toggleFavoriteChannel(channel, $event)
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<mat-list-item
|
||||
><strong>{{
|
||||
'CHANNELS.NO_FAVORITES' | translate
|
||||
}}</strong></mat-list-item
|
||||
>
|
||||
<mat-list-item>
|
||||
{{
|
||||
'CHANNELS.USE_STAR_TO_FAVORITE' | translate
|
||||
}}</mat-list-item
|
||||
>
|
||||
}
|
||||
</mat-nav-list>
|
||||
}
|
||||
</mat-tab>
|
||||
}
|
||||
</mat-tab-group>
|
||||
} @else {
|
||||
No channels there
|
||||
|
||||
+20
-7
@@ -1,4 +1,8 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import {
|
||||
CdkDragDrop,
|
||||
DragDropModule,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { CommonModule, TitleCasePipe } from '@angular/common';
|
||||
import {
|
||||
@@ -22,6 +26,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import * as _ from 'lodash';
|
||||
import { map, skipWhile } from 'rxjs';
|
||||
import { Channel } from '../../../../../shared/channel.interface';
|
||||
import { EpgService } from '../../../services/epg.service';
|
||||
import { FilterPipe } from '../../../shared/pipes/filter.pipe';
|
||||
import * as PlaylistActions from '../../../state/actions';
|
||||
import {
|
||||
@@ -36,8 +41,11 @@ import { ChannelListItemComponent } from './channel-list-item/channel-list-item.
|
||||
templateUrl: './channel-list-container.component.html',
|
||||
styleUrls: ['./channel-list-container.component.scss'],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ChannelListItemComponent,
|
||||
CommonModule,
|
||||
DragDropModule,
|
||||
FilterPipe,
|
||||
FormsModule,
|
||||
MatDividerModule,
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
@@ -45,11 +53,9 @@ import { ChannelListItemComponent } from './channel-list-item/channel-list-item.
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatTabsModule,
|
||||
TranslateModule,
|
||||
TitleCasePipe,
|
||||
FilterPipe,
|
||||
FormsModule,
|
||||
ScrollingModule,
|
||||
TitleCasePipe,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class ChannelListContainerComponent {
|
||||
@@ -116,7 +122,8 @@ export class ChannelListContainerComponent {
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private snackBar: MatSnackBar,
|
||||
private translateService: TranslateService
|
||||
private translateService: TranslateService,
|
||||
private epgService: EpgService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -126,6 +133,12 @@ export class ChannelListContainerComponent {
|
||||
selectChannel(channel: Channel): void {
|
||||
this.selected = channel;
|
||||
this.store.dispatch(PlaylistActions.setActiveChannel({ channel }));
|
||||
|
||||
const epgChannelId = channel?.tvg?.id || channel?.name;
|
||||
|
||||
if (epgChannelId) {
|
||||
this.epgService.getChannelPrograms(epgChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }}
|
||||
</div>
|
||||
<p data-test="title">{{ epgProgram.title[0].value }}</p>
|
||||
<p data-test="title">{{ epgProgram.title }}</p>
|
||||
<div class="subheading-2" *ngIf="epgProgram.title[0].lang">
|
||||
{{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
|
||||
</div>
|
||||
@@ -16,13 +16,13 @@
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }}
|
||||
</div>
|
||||
<p data-test="category">{{ epgProgram.category[0].value }}</p>
|
||||
<p data-test="category">{{ epgProgram.category }}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="epgProgram?.desc?.length > 0">
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }}
|
||||
</div>
|
||||
<p data-test="desc">{{ epgProgram.desc[0].value }}</p>
|
||||
<p data-test="desc">{{ epgProgram.desc }}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="epgProgram?.rating?.length > 0">
|
||||
<div class="subheading-2">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
>
|
||||
}
|
||||
|
||||
@if (item.desc.length > 0) {
|
||||
@if (item.desc?.length > 0) {
|
||||
<span
|
||||
(click)="$event.stopPropagation(); showDescription(item)"
|
||||
matListItemMeta
|
||||
|
||||
@@ -1,118 +1,105 @@
|
||||
@if (channel) {
|
||||
<!-- channel info panel -->
|
||||
<div id="channel-header" color="primary">
|
||||
<div class="channel-info">
|
||||
<div class="channel-icon">
|
||||
@if (channel?.icon) {
|
||||
<img [src]="channel.icon" width="48" />
|
||||
}
|
||||
</div>
|
||||
<div class="channel-details">
|
||||
<div class="channel-name">
|
||||
{{ channel?.name[0]?.value }}
|
||||
</div>
|
||||
<div class="program-name">
|
||||
{{
|
||||
playingNow
|
||||
? playingNow.title[0]?.value
|
||||
: ('EPG.LIVE_STREAM' | translate)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@let items = filteredItems$ | async;
|
||||
@let timeshiftUntil = timeshiftUntil$ | async;
|
||||
|
||||
<!-- channel info panel -->
|
||||
<div id="channel-header" color="primary">
|
||||
<div class="channel-info">
|
||||
<div class="channel-icon">
|
||||
@if (channel?.icon) {
|
||||
<img [src]="channel.icon" width="48" />
|
||||
}
|
||||
</div>
|
||||
<mat-divider />
|
||||
<div id="date-navigator">
|
||||
<button
|
||||
class="previous-day"
|
||||
mat-icon-button
|
||||
(click)="changeDate('prev')"
|
||||
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
|
||||
>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<span class="selected-date">
|
||||
{{ dateToday | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }}</span
|
||||
>
|
||||
<button
|
||||
class="next-day"
|
||||
mat-icon-button
|
||||
(click)="changeDate('next')"
|
||||
[matTooltip]="'EPG.NEXT_DAY' | translate"
|
||||
>
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
<div class="channel-details">
|
||||
<div class="channel-name">
|
||||
{{ channel?.name }}
|
||||
</div>
|
||||
<div class="program-name">
|
||||
{{
|
||||
playingNow
|
||||
? playingNow.title
|
||||
: ('EPG.LIVE_STREAM' | translate)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider />
|
||||
|
||||
@let timeshiftUntil = timeshiftUntil$ | async;
|
||||
@if (timeshiftUntil) {
|
||||
<!-- program list -->
|
||||
<mat-selection-list
|
||||
id="program-list"
|
||||
[multiple]="false"
|
||||
hideSingleSelectionIndicator="true"
|
||||
<div id="date-navigator">
|
||||
<button
|
||||
class="previous-day"
|
||||
mat-icon-button
|
||||
(click)="changeDate('prev')"
|
||||
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
|
||||
>
|
||||
@if (items?.length > 0) {
|
||||
<ng-container *ngFor="let item of items">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<span class="selected-date">
|
||||
{{ dateToday | momentDate: 'YYYY-MM-DD' : 'MMMM Do, dddd' }}</span
|
||||
>
|
||||
<button
|
||||
class="next-day"
|
||||
mat-icon-button
|
||||
(click)="changeDate('next')"
|
||||
[matTooltip]="'EPG.NEXT_DAY' | translate"
|
||||
>
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider />
|
||||
|
||||
@if (timeshiftUntil) {
|
||||
<!-- program list -->
|
||||
<mat-selection-list
|
||||
id="program-list"
|
||||
[multiple]="false"
|
||||
hideSingleSelectionIndicator="true"
|
||||
>
|
||||
@if (items?.length > 0) {
|
||||
@for (program of items; track program) {
|
||||
@if (
|
||||
program.start < timeshiftUntil || program.start >= timeNow
|
||||
) {
|
||||
<mat-list-option>
|
||||
<div matListItemLine>
|
||||
<app-epg-list-item
|
||||
[timeshiftUntil]="timeshiftUntil"
|
||||
[timeNow]="timeNow"
|
||||
[item]="program"
|
||||
/>
|
||||
</div>
|
||||
<p matListItemLine>{{ program?.title }}</p>
|
||||
</mat-list-option>
|
||||
} @else {
|
||||
<mat-list-option
|
||||
*ngIf="
|
||||
item.start < timeshiftUntil ||
|
||||
item.start >= timeNow;
|
||||
else timeshiftItem
|
||||
[value]="program"
|
||||
[class.active]="
|
||||
timeNow >= program.start && timeNow <= program.stop
|
||||
"
|
||||
(click)="
|
||||
setEpgProgram(
|
||||
program,
|
||||
timeNow >= program.start &&
|
||||
timeNow <= program.stop,
|
||||
program.start > timeshiftUntil
|
||||
)
|
||||
"
|
||||
>
|
||||
<div matListItemLine>
|
||||
<app-epg-list-item
|
||||
[timeshiftUntil]="timeshiftUntil"
|
||||
[timeNow]="timeNow"
|
||||
[item]="item"
|
||||
[item]="program"
|
||||
/>
|
||||
</div>
|
||||
<p matListItemLine>{{ item?.title[0]?.value }}</p>
|
||||
<p matListItemLine [innerHTML]="program.title"></p>
|
||||
</mat-list-option>
|
||||
<ng-template #timeshiftItem>
|
||||
<mat-list-option
|
||||
[value]="item"
|
||||
[class.active]="
|
||||
timeNow >= item.start && timeNow <= item.stop
|
||||
"
|
||||
(click)="
|
||||
setEpgProgram(
|
||||
item,
|
||||
timeNow >= item.start &&
|
||||
timeNow <= item.stop,
|
||||
item.start > timeshiftUntil
|
||||
)
|
||||
"
|
||||
>
|
||||
<div matListItemLine>
|
||||
<app-epg-list-item
|
||||
[timeshiftUntil]="timeshiftUntil"
|
||||
[timeNow]="timeNow"
|
||||
[item]="item"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
matListItemLine
|
||||
[innerHTML]="item?.title[0]?.value"
|
||||
></p>
|
||||
</mat-list-option>
|
||||
</ng-template>
|
||||
<mat-divider />
|
||||
</ng-container>
|
||||
} @else {
|
||||
<p class="epg-not-available">
|
||||
{{ 'EPG.EPG_NOT_AVAILABLE_DATE' | translate }}
|
||||
</p>
|
||||
}
|
||||
<mat-divider />
|
||||
}
|
||||
</mat-selection-list>
|
||||
}
|
||||
} @else {
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
{{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_TITLE' | translate }}<br />
|
||||
{{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION' | translate }}
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
} @else {
|
||||
<p class="epg-not-available">
|
||||
{{ 'EPG.EPG_NOT_AVAILABLE_DATE' | translate }}
|
||||
</p>
|
||||
}
|
||||
</mat-selection-list>
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.active {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, NgZone } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
@@ -9,10 +9,11 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import moment from 'moment';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { EpgService } from '../../../services/epg.service';
|
||||
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
|
||||
import {
|
||||
resetActiveEpgProgram,
|
||||
@@ -29,8 +30,7 @@ export interface EpgData {
|
||||
items: EpgProgram[];
|
||||
}
|
||||
|
||||
const DATE_FORMAT = 'YYYYMMDD';
|
||||
const DATE_TIME_FORMAT = 'YYYYMMDDHHmm ZZ';
|
||||
const DATE_FORMAT = 'YYYY-MM-DD';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -58,7 +58,7 @@ export class EpgListComponent {
|
||||
dateToday: string;
|
||||
|
||||
/** Array with EPG programs */
|
||||
items: EpgProgram[] = [];
|
||||
items$ = this.epgService.currentEpgPrograms$;
|
||||
|
||||
/** Object with epg programs for the active channel */
|
||||
programs: {
|
||||
@@ -77,61 +77,65 @@ export class EpgListComponent {
|
||||
/** Timeshift availability date, based on tvg-rec value from the channel */
|
||||
timeshiftUntil$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Creates an instance of EpgListComponent
|
||||
* @param store
|
||||
* @param electronService
|
||||
* @param ngZone
|
||||
*/
|
||||
private readonly selectedDate$ = new BehaviorSubject<string>(
|
||||
moment().format(DATE_FORMAT)
|
||||
);
|
||||
|
||||
/** Filtered EPG programs based on selected date */
|
||||
filteredItems$ = combineLatest([this.items$, this.selectedDate$]).pipe(
|
||||
map(([items, selectedDate]) =>
|
||||
items
|
||||
.filter(
|
||||
(item) =>
|
||||
moment(item.start).format('YYYY-MM-DD') === selectedDate
|
||||
)
|
||||
.sort((a, b) => moment(a.start).diff(moment(b.start)))
|
||||
)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private electronService: DataService,
|
||||
private ngZone: NgZone
|
||||
) {
|
||||
this.electronService.listenOn(
|
||||
EPG_GET_PROGRAM_DONE,
|
||||
(event, response) => {
|
||||
this.ngZone.run(() => this.handleEpgData(response));
|
||||
}
|
||||
);
|
||||
}
|
||||
private dataService: DataService,
|
||||
private readonly epgService: EpgService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Subscribe for values from the store on component init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.timeshiftUntil$ = this.store.select(selectActive).pipe(
|
||||
// eslint-disable-next-line @ngrx/avoid-mapping-selectors
|
||||
map((active) => {
|
||||
console.log(active);
|
||||
this.channel = {
|
||||
id: active?.tvg?.id,
|
||||
name: active?.name,
|
||||
url: [active?.url],
|
||||
icon: [active?.tvg?.logo],
|
||||
};
|
||||
return (
|
||||
active?.tvg?.rec ||
|
||||
active?.timeshift ||
|
||||
active?.catchup?.days
|
||||
);
|
||||
}),
|
||||
map((value) =>
|
||||
moment(Date.now())
|
||||
.subtract(value, 'days')
|
||||
.format(DATE_TIME_FORMAT)
|
||||
)
|
||||
map((value) => moment().subtract(value, 'days').toISOString())
|
||||
);
|
||||
|
||||
this.items$.subscribe((programs) => this.handleEpgData(programs));
|
||||
this.dateToday = moment().format(DATE_FORMAT);
|
||||
this.selectedDate$.next(this.dateToday);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming epg programs for the active channel from the main process
|
||||
* @param programs
|
||||
*/
|
||||
handleEpgData(programs: { payload: EpgData }): void {
|
||||
if (programs?.payload?.items?.length > 0) {
|
||||
this.programs = programs;
|
||||
this.timeNow = moment(Date.now()).format(DATE_TIME_FORMAT);
|
||||
this.dateToday = moment(Date.now()).format(DATE_FORMAT);
|
||||
this.channel = programs.payload?.channel;
|
||||
this.items = this.selectPrograms(programs);
|
||||
|
||||
handleEpgData(programs: EpgProgram[]): void {
|
||||
this.timeNow = new Date().toISOString();
|
||||
this.dateToday = moment().format(DATE_FORMAT);
|
||||
if (programs.length > 0) {
|
||||
this.setPlayingNow();
|
||||
} else {
|
||||
this.items = [];
|
||||
this.channel = null;
|
||||
this.store.dispatch(setCurrentEpgProgram(undefined));
|
||||
}
|
||||
@@ -139,23 +143,20 @@ export class EpgListComponent {
|
||||
|
||||
/**
|
||||
* Selects the program based on the active date
|
||||
* @param programs object with all available epg programs for the active channel
|
||||
*/
|
||||
selectPrograms(programs: { payload: EpgData }): EpgProgram[] {
|
||||
const selectedDate = moment(this.dateToday).format('YYYY-MM-DD');
|
||||
return programs.payload?.items
|
||||
.filter((item) => item.start.includes(this.dateToday.toString()))
|
||||
.filter(
|
||||
(item) =>
|
||||
moment(item.start).format('YYYY-MM-DD') === selectedDate
|
||||
)
|
||||
.map((program) => ({
|
||||
...program,
|
||||
start: moment(program.start, DATE_TIME_FORMAT).format(
|
||||
DATE_TIME_FORMAT
|
||||
),
|
||||
stop: moment(program.stop, DATE_TIME_FORMAT).format(
|
||||
DATE_TIME_FORMAT
|
||||
),
|
||||
start: program.start, // Keep ISO format
|
||||
stop: program.stop, // Keep ISO format
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
return a.start.localeCompare(b.start);
|
||||
});
|
||||
.sort((a, b) => moment(a.start).diff(moment(b.start)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,28 +164,37 @@ export class EpgListComponent {
|
||||
* @param direction direction to switch
|
||||
*/
|
||||
changeDate(direction: 'next' | 'prev'): void {
|
||||
let dateToSwitch;
|
||||
if (direction === 'next') {
|
||||
dateToSwitch = moment(this.dateToday, DATE_FORMAT)
|
||||
.add(1, 'days')
|
||||
.format(DATE_FORMAT);
|
||||
} else if (direction === 'prev') {
|
||||
dateToSwitch = moment(this.dateToday, DATE_FORMAT)
|
||||
.subtract(1, 'days')
|
||||
.format(DATE_FORMAT);
|
||||
}
|
||||
this.dateToday = dateToSwitch;
|
||||
this.items = this.selectPrograms(this.programs);
|
||||
const newDate = moment(this.selectedDate$.value)
|
||||
[direction === 'next' ? 'add' : 'subtract'](1, 'days')
|
||||
.format(DATE_FORMAT);
|
||||
|
||||
this.dateToday = newDate;
|
||||
this.selectedDate$.next(newDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the playing now variable based on the current time
|
||||
*/
|
||||
setPlayingNow(): void {
|
||||
this.playingNow = this.items.find(
|
||||
(item) => this.timeNow >= item.start && this.timeNow <= item.stop
|
||||
);
|
||||
this.store.dispatch(setCurrentEpgProgram({ program: this.playingNow }));
|
||||
this.items$
|
||||
.pipe(
|
||||
map((items) =>
|
||||
items.find((item) => {
|
||||
const now = new Date().toISOString();
|
||||
const start = new Date(item.start).toISOString();
|
||||
const stop = new Date(item.stop).toISOString();
|
||||
return now >= start && now <= stop;
|
||||
})
|
||||
)
|
||||
)
|
||||
.subscribe((playingNow) => {
|
||||
this.playingNow = playingNow;
|
||||
if (playingNow) {
|
||||
this.store.dispatch(
|
||||
setCurrentEpgProgram({ program: playingNow })
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,6 +221,6 @@ export class EpgListComponent {
|
||||
* Removes all ipc renderer listeners after destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.electronService.removeAllListeners(EPG_GET_PROGRAM_DONE);
|
||||
this.dataService.removeAllListeners(EPG_GET_PROGRAM_DONE);
|
||||
}
|
||||
}
|
||||
@@ -75,32 +75,61 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (channel.url) {
|
||||
const url = channel.url + (channel.epgParams ?? '');
|
||||
const extension = getExtensionFromUrl(channel.url);
|
||||
this.dataService.sendIpcEvent(CHANNEL_SET_USER_AGENT, {
|
||||
userAgent: channel.http?.['user-agent'] ?? '',
|
||||
referer: channel.http?.referrer ?? '',
|
||||
});
|
||||
|
||||
if (
|
||||
extension !== 'mp4' &&
|
||||
extension !== 'mpv' &&
|
||||
Hls &&
|
||||
Hls.isSupported()
|
||||
) {
|
||||
console.log('... switching channel to ', channel.name, url);
|
||||
this.hls = new Hls();
|
||||
this.hls.attachMedia(this.videoPlayer.nativeElement);
|
||||
this.hls.loadSource(url);
|
||||
|
||||
this.handlePlayOperation();
|
||||
} else {
|
||||
console.error('something wrong with hls.js init...');
|
||||
this.addSourceToVideo(
|
||||
this.videoPlayer.nativeElement,
|
||||
url,
|
||||
'video/mp4'
|
||||
);
|
||||
this.videoPlayer.nativeElement.play();
|
||||
}
|
||||
// Send IPC event and handle the response
|
||||
this.dataService
|
||||
.sendIpcEvent(CHANNEL_SET_USER_AGENT, {
|
||||
userAgent: channel.http?.['user-agent'] ?? '',
|
||||
referer: channel.http?.referrer ?? '',
|
||||
})
|
||||
.then(() => {
|
||||
if (
|
||||
extension !== 'mp4' &&
|
||||
extension !== 'mpv' &&
|
||||
Hls &&
|
||||
Hls.isSupported()
|
||||
) {
|
||||
console.log(
|
||||
'... switching channel to ',
|
||||
channel.name,
|
||||
url
|
||||
);
|
||||
this.hls = new Hls();
|
||||
this.hls.attachMedia(this.videoPlayer.nativeElement);
|
||||
this.hls.loadSource(url);
|
||||
this.handlePlayOperation();
|
||||
} else {
|
||||
console.log('Using native video player...');
|
||||
this.addSourceToVideo(
|
||||
this.videoPlayer.nativeElement,
|
||||
url,
|
||||
'video/mp4'
|
||||
);
|
||||
this.videoPlayer.nativeElement.play();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error setting user agent:', error);
|
||||
// Continue playback even if setting user agent fails
|
||||
if (
|
||||
extension !== 'mp4' &&
|
||||
extension !== 'mpv' &&
|
||||
Hls &&
|
||||
Hls.isSupported()
|
||||
) {
|
||||
this.hls = new Hls();
|
||||
this.hls.attachMedia(this.videoPlayer.nativeElement);
|
||||
this.hls.loadSource(url);
|
||||
this.handlePlayOperation();
|
||||
} else {
|
||||
this.addSourceToVideo(
|
||||
this.videoPlayer.nativeElement,
|
||||
url,
|
||||
'video/mp4'
|
||||
);
|
||||
this.videoPlayer.nativeElement.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
>
|
||||
<div class="channel-logo">
|
||||
<img
|
||||
[src]="epgProgram?.icon[0] || channel?.tvg?.logo"
|
||||
[src]="epgProgram?.icon || channel?.tvg?.logo"
|
||||
width="68"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="program-name">
|
||||
{{ epgProgram?.title[0]?.value }} | {{ channel.name }}
|
||||
{{ epgProgram?.title }} | {{ channel.name }}
|
||||
</div>
|
||||
<div class="program-description">
|
||||
{{ epgProgram?.desc[0]?.value }}
|
||||
{{ epgProgram?.desc }}
|
||||
</div>
|
||||
<div class="program-progress">
|
||||
<div
|
||||
|
||||
@@ -50,23 +50,23 @@
|
||||
[attr.y]="barHeight * i + barHeight"
|
||||
stroke="black"
|
||||
></rect>
|
||||
@if (item.icon[0]) {
|
||||
@if (item.icon) {
|
||||
<image
|
||||
[attr.y]="barHeight * i + barHeight + 1"
|
||||
[attr.href]="item.icon[0]"
|
||||
[attr.href]="item.icon"
|
||||
[attr.height]="barHeight - 2"
|
||||
width="100"
|
||||
[matTooltip]="item.name[0].value"
|
||||
[matTooltip]="item.name"
|
||||
></image>
|
||||
}
|
||||
@if (!item.icon[0]) {
|
||||
@if (!item.icon) {
|
||||
<foreignObject
|
||||
[attr.y]="barHeight * i + barHeight"
|
||||
width="100"
|
||||
[attr.height]="barHeight"
|
||||
>
|
||||
<div class="channel-name">
|
||||
{{ item.name[0].value }}
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</foreignObject>
|
||||
}
|
||||
@@ -99,7 +99,7 @@
|
||||
<g *ngFor="let item of channels; let i = index">
|
||||
<g
|
||||
*ngFor="let program of item.programs; let a = index"
|
||||
[matTooltip]="program.title[0].value"
|
||||
[matTooltip]="program.title"
|
||||
class="program-item"
|
||||
>
|
||||
<!-- program item -->
|
||||
@@ -118,7 +118,7 @@
|
||||
[attr.x]="program.startPosition"
|
||||
>
|
||||
<div
|
||||
[innerHtml]="program.title[0]?.value"
|
||||
[innerHtml]="program.title"
|
||||
class="program-name"
|
||||
></div>
|
||||
</foreignObject>
|
||||
|
||||
@@ -28,7 +28,7 @@ rect {
|
||||
|
||||
#epg-container {
|
||||
width: calc(100vw - 100px);
|
||||
height: calc(100vh - 73px);
|
||||
height: calc(100vh - 42px);
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
@@ -53,7 +53,6 @@ rect {
|
||||
#epg-navigation {
|
||||
display: flex;
|
||||
background-color: white;
|
||||
margin-top: 30px;
|
||||
border-bottom: 2px solid #fff;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -7,23 +7,32 @@
|
||||
>
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="(playlistId$ | async) !== 'GLOBAL_FAVORITES'"
|
||||
mat-icon-button
|
||||
(click)="addToFavorites(activeChannel)"
|
||||
[matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate"
|
||||
>
|
||||
<mat-icon>
|
||||
{{
|
||||
(favorites$ | async).includes(activeChannel?.id)
|
||||
? 'star'
|
||||
: 'star_outline'
|
||||
}}</mat-icon
|
||||
@if ((playlistId$ | async) !== 'GLOBAL_FAVORITES') {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="addToFavorites(activeChannel)"
|
||||
[matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate"
|
||||
>
|
||||
</button>
|
||||
<mat-icon>
|
||||
{{
|
||||
(favorites$ | async).includes(activeChannel?.id)
|
||||
? 'star'
|
||||
: 'star_outline'
|
||||
}}</mat-icon
|
||||
>
|
||||
</button>
|
||||
}
|
||||
{{ activeChannel?.name }}
|
||||
<div class="spacer"></div>
|
||||
<ng-container *ngIf="isEpgAvailable$ | async">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="openSettings()"
|
||||
[matTooltip]="'MENU.SETTINGS' | translate"
|
||||
>
|
||||
<mat-icon>settings</mat-icon>
|
||||
</button>
|
||||
@let isEpgAvailable = isEpgAvailable$ | async;
|
||||
@if (isEpgAvailable) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="multiEpgClicked.emit()"
|
||||
@@ -38,5 +47,5 @@
|
||||
>
|
||||
EPG
|
||||
</button>
|
||||
</ng-container>
|
||||
}
|
||||
</mat-toolbar>
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AsyncPipe, CommonModule } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
@@ -8,6 +9,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { Channel } from '../../../../../../shared/channel.interface';
|
||||
import { SettingsComponent } from '../../../../settings/settings.component';
|
||||
import { updateFavorites } from '../../../../state/actions';
|
||||
import {
|
||||
selectActivePlaylistId,
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
MatToolbarModule,
|
||||
MatTooltipModule,
|
||||
TranslateModule,
|
||||
MatDialogModule,
|
||||
],
|
||||
selector: 'app-toolbar',
|
||||
templateUrl: './toolbar.component.html',
|
||||
@@ -43,7 +46,8 @@ export class ToolbarComponent {
|
||||
constructor(
|
||||
private snackBar: MatSnackBar,
|
||||
private store: Store,
|
||||
private translateService: TranslateService
|
||||
private translateService: TranslateService,
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -58,4 +62,12 @@ export class ToolbarComponent {
|
||||
);
|
||||
this.store.dispatch(updateFavorites({ channel }));
|
||||
}
|
||||
|
||||
openSettings(): void {
|
||||
this.dialog.open(SettingsComponent, {
|
||||
width: '1000px',
|
||||
height: '90%',
|
||||
data: { isDialog: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,6 @@
|
||||
</mat-drawer-content>
|
||||
<!-- right sidebar content -->
|
||||
<mat-drawer position="end" #drawerRight mode="side" disableClose>
|
||||
<app-epg-list *ngIf="isElectron" />
|
||||
<app-epg-list *ngIf="isTauri" />
|
||||
</mat-drawer>
|
||||
</mat-drawer-container>
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
effect,
|
||||
} from '@angular/core';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
import { Playlist } from '../../../../../shared/playlist.interface';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { PlaylistsService } from '../../../services/playlists.service';
|
||||
import { SettingsStore } from '../../../services/settings-store.service';
|
||||
import { Settings, VideoPlayer } from '../../../settings/settings.interface';
|
||||
import { STORE_KEY } from '../../../shared/enums/store-keys.enum';
|
||||
import * as PlaylistActions from '../../../state/actions';
|
||||
@@ -118,7 +120,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
|
||||
listeners = [];
|
||||
|
||||
isElectron = this.dataService.isElectron;
|
||||
isTauri = this.dataService.getAppEnvironment() === 'tauri';
|
||||
|
||||
sidebarView: SidebarView = 'CHANNELS';
|
||||
|
||||
@@ -136,13 +138,22 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private storage: StorageMap,
|
||||
private store: Store
|
||||
private store: Store,
|
||||
private settingsStore: SettingsStore
|
||||
) {
|
||||
// Initialize volume from localStorage in constructor
|
||||
const savedVolume = localStorage.getItem('volume');
|
||||
if (savedVolume !== null) {
|
||||
this.volume = Number(savedVolume);
|
||||
}
|
||||
|
||||
// React to settings changes
|
||||
effect(() => {
|
||||
this.playerSettings = {
|
||||
player: this.settingsStore.player(),
|
||||
showCaptions: this.settingsStore.showCaptions(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +221,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
setRendererListeners(): void {
|
||||
this.commandsList.forEach((command) => {
|
||||
if (this.isElectron) {
|
||||
if (this.isTauri) {
|
||||
this.dataService.listenOn(command.id, (event, response) =>
|
||||
this.ngZone.run(() => command.execute(response))
|
||||
);
|
||||
@@ -243,7 +254,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.isElectron) {
|
||||
if (this.isTauri) {
|
||||
this.dataService.removeAllListeners(PLAYLIST_PARSE_RESPONSE);
|
||||
} else {
|
||||
this.listeners.forEach((listener) =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface EpgChannel {
|
||||
id: string;
|
||||
name: { lang: string; value: string }[];
|
||||
name: string;
|
||||
icon: string[];
|
||||
url: string[];
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import { EpgItem } from '../../xtream/epg-item.interface';
|
||||
templateUrl: './epg-view.component.html',
|
||||
imports: [
|
||||
DatePipe,
|
||||
EpgItemDescriptionComponent,
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
@@ -24,8 +23,8 @@ import { EpgItem } from '../../xtream/epg-item.interface';
|
||||
],
|
||||
styles: `
|
||||
mat-list {
|
||||
overflow-y: scroll;
|
||||
height: calc(100vh - 459px);
|
||||
/* overflow-y: scroll; */
|
||||
height: 100vh;
|
||||
}
|
||||
`,
|
||||
})
|
||||
@@ -37,8 +36,8 @@ export class EpgViewComponent {
|
||||
showDetails(item: EpgItem) {
|
||||
this.dialog.open(EpgItemDescriptionComponent, {
|
||||
data: {
|
||||
title: [{ value: item.title, lang: item.lang }],
|
||||
desc: [{ value: item.description }],
|
||||
title: item.title ?? 'No title',
|
||||
desc: item.description ?? 'No description',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
.itv-container {
|
||||
display: grid;
|
||||
grid-template-columns: 330px 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
grid-template-rows: 50% 50%;
|
||||
gap: 0 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'sidebar video-player video-player'
|
||||
'sidebar epg epg'
|
||||
'sidebar epg epg';
|
||||
height: 100%;
|
||||
}
|
||||
@@ -49,9 +48,10 @@
|
||||
}
|
||||
|
||||
.epg-content {
|
||||
height: 100%;
|
||||
height: calc(50vh - 20px);
|
||||
overflow-y: auto;
|
||||
border-right: #111 1px solid;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
app-vjs-player > .video-js {
|
||||
height: 100%;
|
||||
height: 50vh !important;
|
||||
}
|
||||
|
||||
app-html-video-player > #video-player {
|
||||
height: 50vh !important;
|
||||
}
|
||||
@@ -1,66 +1,125 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { EPG_FETCH } from '../../../shared/ipc-commands';
|
||||
import { setEpgAvailableFlag } from '../state/actions';
|
||||
import { DataService } from './data.service';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { BehaviorSubject, from } from 'rxjs';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
import { EpgProgram } from '../player/models/epg-program.model';
|
||||
import * as PlaylistActions from '../state/actions';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EpgService {
|
||||
/** Default options for epg snackbar notifications */
|
||||
epgSnackBarOptions: MatSnackBarConfig = {
|
||||
verticalPosition: 'bottom',
|
||||
horizontalPosition: 'start',
|
||||
};
|
||||
private epgAvailable = new BehaviorSubject<boolean>(false);
|
||||
private currentEpgPrograms = new BehaviorSubject<EpgProgram[]>([]);
|
||||
|
||||
epgAvailable$ = this.epgAvailable.asObservable();
|
||||
currentEpgPrograms$ = this.currentEpgPrograms.asObservable();
|
||||
|
||||
constructor(
|
||||
private store: Store,
|
||||
private electronService: DataService,
|
||||
private snackBar: MatSnackBar,
|
||||
private translate: TranslateService
|
||||
private translate: TranslateService,
|
||||
private store: Store
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fetches and updates EPG from the given sources
|
||||
* @param urls epg source urls
|
||||
* Fetches EPG from the given URLs
|
||||
*/
|
||||
fetchEpg(urls: string | string[]) {
|
||||
const urlsArray = Array.isArray(urls) ? urls : [urls];
|
||||
urlsArray.forEach((url) =>
|
||||
this.electronService.sendIpcEvent(EPG_FETCH, {
|
||||
url,
|
||||
})
|
||||
);
|
||||
fetchEpg(urls: string[]): void {
|
||||
this.showFetchSnackbar();
|
||||
|
||||
// Filter out empty URLs and send all URLs at once
|
||||
const validUrls = urls.filter((url) => url?.trim());
|
||||
if (validUrls.length === 0) return;
|
||||
|
||||
from(invoke('fetch_epg', { url: validUrls }))
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.epgAvailable.next(true);
|
||||
this.showSuccessSnackbar();
|
||||
}),
|
||||
catchError((err) => {
|
||||
console.error('EPG fetch error:', err);
|
||||
this.epgAvailable.next(false);
|
||||
this.showErrorSnackbar();
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
showFetchSnackbar() {
|
||||
/**
|
||||
* Gets EPG programs for a specific channel
|
||||
*/
|
||||
getChannelPrograms(channelId: string): void {
|
||||
console.log('Fetching EPG for channel ID:', channelId);
|
||||
from(invoke<EpgProgram[]>('get_channel_programs', { channelId }))
|
||||
.pipe(
|
||||
tap((programs) => {
|
||||
console.log('Received programs:', programs);
|
||||
}),
|
||||
map((programs) =>
|
||||
programs.map((program) => ({
|
||||
...program,
|
||||
start: new Date(program.start).toISOString(),
|
||||
stop: new Date(program.stop).toISOString(),
|
||||
}))
|
||||
),
|
||||
catchError((err) => {
|
||||
console.error('EPG get programs error:', err);
|
||||
this.showErrorSnackbar();
|
||||
this.currentEpgPrograms.next([]);
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
.subscribe((programs) => {
|
||||
if (programs.length === 0) {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setEpgAvailableFlag({ value: false })
|
||||
);
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setEpgAvailableFlag({ value: true })
|
||||
);
|
||||
}
|
||||
this.currentEpgPrograms.next(programs);
|
||||
console.log('Updated programs:', programs); // Debug log
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows fetch in progress snackbar
|
||||
*/
|
||||
showFetchSnackbar(): void {
|
||||
this.snackBar.open(this.translate.instant('EPG.FETCH_EPG'), null, {
|
||||
duration: 2000,
|
||||
horizontalPosition: 'start',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows success snackbar
|
||||
*/
|
||||
private showSuccessSnackbar(): void {
|
||||
this.snackBar.open(this.translate.instant('EPG.FETCH_SUCCESS'), null, {
|
||||
duration: 2000,
|
||||
horizontalPosition: 'start',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows error snackbar
|
||||
*/
|
||||
private showErrorSnackbar(): void {
|
||||
this.snackBar.open(
|
||||
this.translate.instant('EPG.FETCH_EPG'),
|
||||
this.translate.instant('EPG.ERROR'),
|
||||
this.translate.instant('CLOSE'),
|
||||
this.epgSnackBarOptions
|
||||
);
|
||||
}
|
||||
|
||||
onEpgFetchDone() {
|
||||
this.store.dispatch(setEpgAvailableFlag({ value: true }));
|
||||
this.snackBar.open(
|
||||
this.translate.instant('EPG.DOWNLOAD_SUCCESS'),
|
||||
null,
|
||||
{
|
||||
...this.epgSnackBarOptions,
|
||||
duration: 2000,
|
||||
horizontalPosition: 'start',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onEpgError() {
|
||||
this.snackBar.open(this.translate.instant('EPG.ERROR'), null, {
|
||||
...this.epgSnackBarOptions,
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -143,20 +143,34 @@ export class PlaylistsService {
|
||||
|
||||
getPortalFavorites(portalId: string) {
|
||||
return this.dbService
|
||||
.getByID<{ favorites: Partial<XtreamItem>[] }>(
|
||||
DbStores.Playlists,
|
||||
portalId
|
||||
)
|
||||
.pipe(map((item) => item.favorites.filter(itm => itm && itm.stream_type && itm.stream_type !== 'live') ?? []));
|
||||
.getByID<{
|
||||
favorites: Partial<XtreamItem>[];
|
||||
}>(DbStores.Playlists, portalId)
|
||||
.pipe(
|
||||
map((item) => {
|
||||
if (!item || !item.favorites) return [];
|
||||
return item.favorites.filter(
|
||||
(itm) =>
|
||||
itm && itm.stream_type && itm.stream_type !== 'live'
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getPortalLiveStreamFavorites(portalId: string) {
|
||||
return this.dbService
|
||||
.getByID<{ favorites: Partial<XtreamItem>[] }>(
|
||||
DbStores.Playlists,
|
||||
portalId
|
||||
)
|
||||
.pipe(map((item) => item.favorites.filter(itm => itm && itm.stream_type && itm.stream_type === 'live') ?? []));
|
||||
.getByID<{
|
||||
favorites: Partial<XtreamItem>[];
|
||||
}>(DbStores.Playlists, portalId)
|
||||
.pipe(
|
||||
map((item) => {
|
||||
if (!item || !item.favorites) return [];
|
||||
return item.favorites.filter(
|
||||
(itm) =>
|
||||
itm && itm.stream_type && itm.stream_type === 'live'
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addPortalFavorite(portalId: string, item: any) {
|
||||
@@ -217,6 +231,7 @@ export class PlaylistsService {
|
||||
) {
|
||||
try {
|
||||
const parsedPlaylist = parse(playlist);
|
||||
console.log(parsedPlaylist);
|
||||
return createPlaylistObject(
|
||||
title,
|
||||
parsedPlaylist,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ApplicationRef, inject, Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -19,23 +19,21 @@ import { Playlist } from '../../../shared/playlist.interface';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
import * as PlaylistActions from '../state/actions';
|
||||
import { DataService } from './data.service';
|
||||
import { PlaylistsService } from './playlists.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PwaService extends DataService {
|
||||
appRef = inject(ApplicationRef);
|
||||
playlistService = inject(PlaylistsService);
|
||||
snackBar = inject(MatSnackBar);
|
||||
store = inject(Store);
|
||||
swUpdate = inject(SwUpdate);
|
||||
translateService = inject(TranslateService);
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly store = inject(Store);
|
||||
private readonly swUpdate = inject(SwUpdate);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
|
||||
/** Proxy URL to avoid CORS issues */
|
||||
corsProxyUrl = AppConfig.BACKEND_URL;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
constructor() {
|
||||
super();
|
||||
console.log('PWA service initialized...');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Injectable, computed, signal } from '@angular/core';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { Language } from '../settings/language.enum';
|
||||
import { Settings, VideoPlayer } from '../settings/settings.interface';
|
||||
import { Theme } from '../settings/theme.enum';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
player: VideoPlayer.VideoJs,
|
||||
language: Language.ENGLISH,
|
||||
showCaptions: false,
|
||||
theme: Theme.LightTheme,
|
||||
mpvPlayerPath: '',
|
||||
vlcPlayerPath: '',
|
||||
remoteControl: false,
|
||||
remoteControlPort: 3000,
|
||||
epgUrl: [],
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SettingsStore {
|
||||
private settings = signal<Settings>(DEFAULT_SETTINGS);
|
||||
|
||||
// Computed values for commonly used settings
|
||||
readonly player = computed(() => this.settings().player);
|
||||
readonly showCaptions = computed(() => this.settings().showCaptions);
|
||||
readonly theme = computed(() => this.settings().theme);
|
||||
|
||||
constructor(private storage: StorageMap) {
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
const stored = await this.storage.get(STORE_KEY.Settings).toPromise();
|
||||
if (stored) {
|
||||
this.settings.set({ ...DEFAULT_SETTINGS, ...(stored as Settings) });
|
||||
}
|
||||
}
|
||||
|
||||
async updateSettings(settings: Partial<Settings>) {
|
||||
const newSettings = { ...this.settings(), ...settings };
|
||||
this.settings.set(newSettings);
|
||||
await this.storage.set(STORE_KEY.Settings, newSettings).toPromise();
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import {
|
||||
EPG_GET_PROGRAM_DONE,
|
||||
ERROR,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
@@ -66,6 +67,11 @@ export class TauriService extends DataService {
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
} else if (type === 'EPG_FETCH_DONE') {
|
||||
window.postMessage({
|
||||
type: EPG_GET_PROGRAM_DONE,
|
||||
payload,
|
||||
});
|
||||
} else {
|
||||
console.log('Unknown type', type);
|
||||
}
|
||||
@@ -184,7 +190,10 @@ export class TauriService extends DataService {
|
||||
}
|
||||
|
||||
removeAllListeners(type: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
console.error(
|
||||
'Method not implemented. Following type was provided:',
|
||||
type
|
||||
);
|
||||
}
|
||||
|
||||
listenOn(command: string, callback: (...args: any[]) => void): void {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<app-header
|
||||
[title]="'SETTINGS.TITLE' | translate"
|
||||
[subtitle]="
|
||||
('SETTINGS.SUBTITLE' | translate) +
|
||||
'. ' +
|
||||
('SETTINGS.DESCRIPTION' | translate)
|
||||
"
|
||||
></app-header>
|
||||
@if (!isDialog) {
|
||||
<app-header
|
||||
[title]="'SETTINGS.TITLE' | translate"
|
||||
[subtitle]="
|
||||
('SETTINGS.SUBTITLE' | translate) +
|
||||
'. ' +
|
||||
('SETTINGS.DESCRIPTION' | translate)
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<h2 mat-dialog-title>{{ 'SETTINGS.TITLE' | translate }}</h2>
|
||||
}
|
||||
|
||||
<div class="settings-container" data-test-id="settings-container">
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()" novalidate>
|
||||
<ng-container formArrayName="epgUrl" *ngIf="isElectron">
|
||||
<ng-container formArrayName="epgUrl" *ngIf="isTauri">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.EPG_URL_LABEL' | translate }}
|
||||
@@ -205,7 +209,7 @@
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
@if (isPwa || isElectron) {
|
||||
@if (isPwa || isTauri) {
|
||||
<mat-divider />
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
@@ -311,12 +315,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" *ngIf="isPwa">
|
||||
{{ 'SETTINGS.EPG_NOTE' | translate }}
|
||||
<a href="https://github.com/4gray/iptvnator/releases">{{
|
||||
'SETTINGS.EPG_NOTE_URL_TEXT' | translate
|
||||
}}</a>
|
||||
</div>
|
||||
@if (isPwa) {
|
||||
<div class="row">
|
||||
{{ 'SETTINGS.EPG_NOTE' | translate }}
|
||||
<a href="https://github.com/4gray/iptvnator/releases">{{
|
||||
'SETTINGS.EPG_NOTE_URL_TEXT' | translate
|
||||
}}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-divider />
|
||||
<div class="action-buttons" align="end">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-base-to-string */
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, Inject, Input, OnInit, Optional } from '@angular/core';
|
||||
import {
|
||||
FormArray,
|
||||
FormBuilder,
|
||||
@@ -10,6 +11,11 @@ import {
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogModule,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
@@ -22,7 +28,6 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { take } from 'rxjs';
|
||||
import * as semver from 'semver';
|
||||
import {
|
||||
EPG_FORCE_FETCH,
|
||||
SET_MPV_PLAYER_PATH,
|
||||
SET_VLC_PLAYER_PATH,
|
||||
SETTINGS_UPDATE,
|
||||
@@ -32,13 +37,13 @@ import { DataService } from '../services/data.service';
|
||||
import { DialogService } from '../services/dialog.service';
|
||||
import { EpgService } from '../services/epg.service';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { SettingsStore } from '../services/settings-store.service';
|
||||
import { HeaderComponent } from '../shared/components/header/header.component';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
import * as PlaylistActions from '../state/actions';
|
||||
import { selectIsEpgAvailable } from '../state/selectors';
|
||||
import { SettingsService } from './../services/settings.service';
|
||||
import { Language } from './language.enum';
|
||||
import { Settings, VideoPlayer } from './settings.interface';
|
||||
import { VideoPlayer } from './settings.interface';
|
||||
import { Theme } from './theme.enum';
|
||||
|
||||
@Component({
|
||||
@@ -58,14 +63,16 @@ import { Theme } from './theme.enum';
|
||||
MatTooltipModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
MatDialogModule,
|
||||
],
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
@Input() isDialog = false;
|
||||
/** List with available languages as enum */
|
||||
languageEnum = Language;
|
||||
|
||||
/** Flag that indicates whether the app runs in electron environment */
|
||||
isElectron = this.electronService.isElectron;
|
||||
isTauri = this.electronService.getAppEnvironment() === 'tauri';
|
||||
|
||||
isPwa = this.electronService.getAppEnvironment() === 'pwa';
|
||||
|
||||
@@ -91,7 +98,7 @@ export class SettingsComponent implements OnInit {
|
||||
label: 'VideoJs Player',
|
||||
},
|
||||
...this.electronPlayers,
|
||||
/* ...(this.isElectron ? this.electronPlayers : []), */
|
||||
/* ...(this.isTauri ? this.electronPlayers : []), */
|
||||
];
|
||||
|
||||
/** Current version of the app */
|
||||
@@ -109,7 +116,7 @@ export class SettingsComponent implements OnInit {
|
||||
/** Settings form object */
|
||||
settingsForm = this.formBuilder.group({
|
||||
player: [VideoPlayer.VideoJs],
|
||||
...(this.isElectron ? { epgUrl: new FormArray([]) } : {}),
|
||||
...(this.isTauri ? { epgUrl: new FormArray([]) } : {}),
|
||||
language: Language.ENGLISH,
|
||||
showCaptions: false,
|
||||
theme: Theme.LightTheme,
|
||||
@@ -136,8 +143,13 @@ export class SettingsComponent implements OnInit {
|
||||
private settingsService: SettingsService,
|
||||
private snackBar: MatSnackBar,
|
||||
private store: Store,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
private translate: TranslateService,
|
||||
private matDialog: MatDialog,
|
||||
private settingsStore: SettingsStore,
|
||||
@Optional() @Inject(MAT_DIALOG_DATA) data?: { isDialog: boolean }
|
||||
) {
|
||||
this.isDialog = data?.isDialog ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the config object from the browsers
|
||||
@@ -152,34 +164,12 @@ export class SettingsComponent implements OnInit {
|
||||
* Sets saved settings from the indexed db store
|
||||
*/
|
||||
setSettings(): void {
|
||||
this.settingsService
|
||||
.getValueFromLocalStorage(STORE_KEY.Settings)
|
||||
.subscribe((settings: Settings) => {
|
||||
if (settings) {
|
||||
try {
|
||||
this.settingsForm.setValue({
|
||||
player: settings.player
|
||||
? settings.player
|
||||
: VideoPlayer.VideoJs,
|
||||
...(this.isElectron ? { epgUrl: [] } : {}),
|
||||
language: settings.language ?? Language.ENGLISH,
|
||||
showCaptions: settings.showCaptions ?? false,
|
||||
theme: settings.theme ?? Theme.LightTheme,
|
||||
mpvPlayerPath: settings.mpvPlayerPath ?? '',
|
||||
vlcPlayerPath: settings.vlcPlayerPath ?? '',
|
||||
remoteControl: settings.remoteControl ?? false,
|
||||
remoteControlPort:
|
||||
settings.remoteControlPort ?? 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
const currentSettings = this.settingsStore.getSettings()();
|
||||
this.settingsForm.patchValue(currentSettings);
|
||||
|
||||
if (this.isElectron) {
|
||||
this.setEpgUrls(settings.epgUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (this.isTauri && currentSettings.epgUrl) {
|
||||
this.setEpgUrls(currentSettings.epgUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,31 +238,26 @@ export class SettingsComponent implements OnInit {
|
||||
* the indexed db store
|
||||
*/
|
||||
onSubmit(): void {
|
||||
this.settingsService
|
||||
.setValueToLocalStorage(
|
||||
STORE_KEY.Settings,
|
||||
this.settingsForm.value,
|
||||
true
|
||||
)
|
||||
.pipe(take(1))
|
||||
.subscribe(() => {
|
||||
this.applyChangedSettings();
|
||||
});
|
||||
this.settingsStore.updateSettings(this.settingsForm.value).then(() => {
|
||||
this.applyChangedSettings();
|
||||
this.electronService.sendIpcEvent(
|
||||
SETTINGS_UPDATE,
|
||||
this.settingsForm.value
|
||||
);
|
||||
|
||||
this.electronService.sendIpcEvent(
|
||||
SETTINGS_UPDATE,
|
||||
this.settingsForm.value
|
||||
);
|
||||
this.electronService.sendIpcEvent(
|
||||
SET_MPV_PLAYER_PATH,
|
||||
this.settingsForm.value.mpvPlayerPath
|
||||
);
|
||||
|
||||
this.electronService.sendIpcEvent(
|
||||
SET_MPV_PLAYER_PATH,
|
||||
this.settingsForm.value.mpvPlayerPath
|
||||
);
|
||||
|
||||
this.electronService.sendIpcEvent(
|
||||
SET_VLC_PLAYER_PATH,
|
||||
this.settingsForm.value.mpvPlayerPath
|
||||
);
|
||||
this.electronService.sendIpcEvent(
|
||||
SET_VLC_PLAYER_PATH,
|
||||
this.settingsForm.value.mpvPlayerPath
|
||||
);
|
||||
});
|
||||
if (this.isDialog) {
|
||||
this.matDialog.closeAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,15 +265,17 @@ export class SettingsComponent implements OnInit {
|
||||
*/
|
||||
applyChangedSettings(): void {
|
||||
this.settingsForm.markAsPristine();
|
||||
// check whether the epg url was changed or not
|
||||
if (this.isElectron) {
|
||||
if (this.isTauri) {
|
||||
let epgUrls = this.settingsForm.value.epgUrl;
|
||||
if (epgUrls) {
|
||||
if (!Array.isArray(epgUrls)) {
|
||||
epgUrls = [epgUrls];
|
||||
}
|
||||
epgUrls = epgUrls.filter((url) => url !== '');
|
||||
this.epgService.fetchEpg(epgUrls);
|
||||
if (epgUrls.length > 0) {
|
||||
// Fetch all EPG URLs at once
|
||||
this.epgService.fetchEpg(epgUrls);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.translate.use(this.settingsForm.value.language);
|
||||
@@ -307,7 +294,11 @@ export class SettingsComponent implements OnInit {
|
||||
* Navigates back to the applications homepage
|
||||
*/
|
||||
backToHome(): void {
|
||||
this.router.navigateByUrl('/');
|
||||
if (this.isDialog) {
|
||||
this.matDialog.closeAll();
|
||||
} else {
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,8 +306,7 @@ export class SettingsComponent implements OnInit {
|
||||
* @param url epg source url
|
||||
*/
|
||||
refreshEpg(url: string): void {
|
||||
this.electronService.sendIpcEvent(EPG_FORCE_FETCH, url);
|
||||
this.epgService.showFetchSnackbar();
|
||||
this.epgService.fetchEpg([url]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -394,6 +384,7 @@ export class SettingsComponent implements OnInit {
|
||||
duration: 2000,
|
||||
}
|
||||
);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
@@ -370,33 +370,36 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
}
|
||||
|
||||
openPlayer(streamUrl: string) {
|
||||
const player = this.settings()?.player ?? VideoPlayer.VideoJs;
|
||||
if (player === VideoPlayer.MPV) {
|
||||
if (!this.hideExternalInfoDialog())
|
||||
this.dialog.open(ExternalPlayerInfoDialogComponent);
|
||||
this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
|
||||
url: streamUrl,
|
||||
mpvPlayerPath: this.settings()?.mpvPlayerPath,
|
||||
});
|
||||
} else if (player === VideoPlayer.VLC) {
|
||||
if (!this.hideExternalInfoDialog())
|
||||
this.dialog.open(ExternalPlayerInfoDialogComponent);
|
||||
this.dataService.sendIpcEvent(OPEN_VLC_PLAYER, {
|
||||
url: streamUrl,
|
||||
vlcPlayerPath: this.settings()?.vlcPlayerPath,
|
||||
});
|
||||
} else {
|
||||
this.dialog.open<PlayerDialogComponent, PlayerDialogData>(
|
||||
PlayerDialogComponent,
|
||||
{
|
||||
data: {
|
||||
streamUrl,
|
||||
title: this.itvTitle,
|
||||
},
|
||||
width: '80%',
|
||||
}
|
||||
);
|
||||
}
|
||||
// Get fresh settings directly from storage instead of using signal
|
||||
this.storage.get(STORE_KEY.Settings).subscribe((settings: Settings) => {
|
||||
const player = settings?.player ?? VideoPlayer.VideoJs;
|
||||
if (player === VideoPlayer.MPV) {
|
||||
if (!this.hideExternalInfoDialog())
|
||||
this.dialog.open(ExternalPlayerInfoDialogComponent);
|
||||
this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
|
||||
url: streamUrl,
|
||||
mpvPlayerPath: settings?.mpvPlayerPath,
|
||||
});
|
||||
} else if (player === VideoPlayer.VLC) {
|
||||
if (!this.hideExternalInfoDialog())
|
||||
this.dialog.open(ExternalPlayerInfoDialogComponent);
|
||||
this.dataService.sendIpcEvent(OPEN_VLC_PLAYER, {
|
||||
url: streamUrl,
|
||||
vlcPlayerPath: settings?.vlcPlayerPath,
|
||||
});
|
||||
} else {
|
||||
this.dialog.open<PlayerDialogComponent, PlayerDialogData>(
|
||||
PlayerDialogComponent,
|
||||
{
|
||||
data: {
|
||||
streamUrl,
|
||||
title: this.itvTitle,
|
||||
},
|
||||
width: '80%',
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
categoryClicked(item: { category_name: string; category_id: string }) {
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
'CHANNELS.FAVORITES' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
|
||||
<button mat-button (click)="openSettings()">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span class="button-label">{{ 'MENU.SETTINGS' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sub-panel">
|
||||
@@ -59,11 +64,7 @@
|
||||
[matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate"
|
||||
>
|
||||
<mat-icon>
|
||||
{{
|
||||
isFavoriteStream
|
||||
? 'star'
|
||||
: 'star_outline'
|
||||
}}
|
||||
{{ isFavoriteStream ? 'star' : 'star_outline' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@@ -91,9 +92,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<mat-menu #sortPlaylistMenu="matMenu">
|
||||
<section
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<section (click)="$event.stopPropagation()">
|
||||
<button mat-menu-item (click)="setSortType('date')">
|
||||
<mat-icon>calendar_today</mat-icon>
|
||||
<span>{{ 'CHANNELS.SORT_BY_DATE' | translate }}</span>
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
import { NgFor, NgIf, AsyncPipe } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output, inject, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable, Subject, debounceTime, distinctUntilChanged, map } from 'rxjs';
|
||||
import {
|
||||
Observable,
|
||||
Subject,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
} from 'rxjs';
|
||||
import { XtreamLiveStream } from '../../../../shared/xtream-live-stream.interface';
|
||||
import { PlaylistInfoComponent } from '../../home/recent-playlists/playlist-info/playlist-info.component';
|
||||
import { SettingsComponent } from '../../settings/settings.component';
|
||||
import { selectCurrentPlaylist } from '../../state/selectors';
|
||||
|
||||
import { SettingsStore } from '../../services/settings-store.service';
|
||||
import { Breadcrumb } from '../breadcrumb.interface';
|
||||
import { ContentTypeNavigationItem } from '../content-type-navigation-item.interface';
|
||||
import { ContentType } from '../content-type.enum';
|
||||
import { PortalStore } from '../portal.store';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { XtreamLiveStream } from '../../../../shared/xtream-live-stream.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navigation-bar',
|
||||
@@ -41,7 +58,7 @@ import { XtreamLiveStream } from '../../../../shared/xtream-live-stream.interfac
|
||||
MatMenuModule,
|
||||
MatCheckboxModule,
|
||||
MatTooltipModule,
|
||||
AsyncPipe
|
||||
AsyncPipe,
|
||||
],
|
||||
})
|
||||
export class NavigationBarComponent implements OnChanges {
|
||||
@@ -62,7 +79,7 @@ export class NavigationBarComponent implements OnChanges {
|
||||
@Output() searchTextChanged = new EventEmitter<string>();
|
||||
@Output() addToFavoritesClicked = new EventEmitter<any>();
|
||||
@Output() removeFromFavoritesClicked = new EventEmitter<number>();
|
||||
|
||||
|
||||
ContentTypeEnum = ContentType;
|
||||
dialog = inject(MatDialog);
|
||||
portalStore = inject(PortalStore);
|
||||
@@ -72,6 +89,7 @@ export class NavigationBarComponent implements OnChanges {
|
||||
currentPlaylist = this.store.selectSignal(selectCurrentPlaylist);
|
||||
sortType = this.portalStore.sortType;
|
||||
isFavoriteStream = false;
|
||||
settingsStore = inject(SettingsStore);
|
||||
|
||||
constructor() {
|
||||
this.searchPhraseUpdate
|
||||
@@ -117,7 +135,7 @@ export class NavigationBarComponent implements OnChanges {
|
||||
name: item.name,
|
||||
stream_id: item.stream_id,
|
||||
cover: item.stream_icon,
|
||||
stream_type: 'live'
|
||||
stream_type: 'live',
|
||||
});
|
||||
this.isFavoriteStream = true;
|
||||
} else {
|
||||
@@ -128,17 +146,36 @@ export class NavigationBarComponent implements OnChanges {
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.activeLiveStream && this.activeLiveStream) {
|
||||
this.checkFavorite();
|
||||
this.checkFavorite();
|
||||
}
|
||||
}
|
||||
|
||||
checkFavorite() {
|
||||
// if activeLiveStream.stream_id include in favorites return the true
|
||||
const activeLiveStream = this.activeLiveStream;
|
||||
this.favoritesLiveStream$.pipe(
|
||||
map(favorites => favorites.some(fav => fav.stream_id === activeLiveStream.stream_id))
|
||||
).subscribe(isFavorite => {
|
||||
this.isFavoriteStream = isFavorite;
|
||||
});
|
||||
this.favoritesLiveStream$
|
||||
.pipe(
|
||||
map((favorites) =>
|
||||
favorites.some(
|
||||
(fav) => fav.stream_id === activeLiveStream.stream_id
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe((isFavorite) => {
|
||||
this.isFavoriteStream = isFavorite;
|
||||
});
|
||||
}
|
||||
|
||||
openSettings(): void {
|
||||
this.dialog
|
||||
.open(SettingsComponent, {
|
||||
width: '1000px',
|
||||
height: '90%',
|
||||
data: { isDialog: true },
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe(() => {
|
||||
this.settingsStore.loadSettings();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,5 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
KeyValuePipe,
|
||||
NgFor,
|
||||
NgIf,
|
||||
NgSwitch,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
NgZone,
|
||||
OnInit,
|
||||
Signal,
|
||||
effect,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, NgZone, OnInit, effect, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
@@ -42,10 +29,9 @@ import { EpgItem } from './epg-item.interface';
|
||||
import { NavigationBarComponent } from './navigation-bar/navigation-bar.component';
|
||||
import { VodDetailsComponent } from './vod-details/vod-details.component';
|
||||
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -56,9 +42,9 @@ import {
|
||||
import { LiveStreamLayoutComponent } from '../portals/live-stream-layout/live-stream-layout.component';
|
||||
import { DialogService } from '../services/dialog.service';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { Settings, VideoPlayer } from '../settings/settings.interface';
|
||||
import { SettingsStore } from '../services/settings-store.service';
|
||||
import { VideoPlayer } from '../settings/settings.interface';
|
||||
import { ExternalPlayerInfoDialogComponent } from '../shared/components/external-player-info-dialog/external-player-info-dialog.component';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
import { PlaylistErrorViewComponent } from '../xtream/playlist-error-view/playlist-error-view.component';
|
||||
import { Breadcrumb, PortalActions } from './breadcrumb.interface';
|
||||
import { ContentTypeNavigationItem } from './content-type-navigation-item.interface';
|
||||
@@ -112,12 +98,8 @@ type LayoutView =
|
||||
styleUrls: ['./xtream-main-container.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
KeyValuePipe,
|
||||
MatButtonToggleModule,
|
||||
NgFor,
|
||||
NgIf,
|
||||
CategoryViewComponent,
|
||||
NgSwitch,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
@@ -125,11 +107,8 @@ type LayoutView =
|
||||
VodDetailsComponent,
|
||||
CategoryContentViewComponent,
|
||||
SerialDetailsComponent,
|
||||
PlayerDialogComponent,
|
||||
MatProgressSpinnerModule,
|
||||
AsyncPipe,
|
||||
ExternalPlayerInfoDialogComponent,
|
||||
RouterLink,
|
||||
PlaylistErrorViewComponent,
|
||||
TranslateModule,
|
||||
LiveStreamLayoutComponent,
|
||||
@@ -143,6 +122,7 @@ export class XtreamMainContainerComponent implements OnInit {
|
||||
playlistService = inject(PlaylistsService);
|
||||
portalStore = inject(PortalStore);
|
||||
router = inject(Router);
|
||||
settingsStore = inject(SettingsStore);
|
||||
snackBar = inject(MatSnackBar);
|
||||
storage = inject(StorageMap);
|
||||
store = inject(Store);
|
||||
@@ -176,9 +156,7 @@ export class XtreamMainContainerComponent implements OnInit {
|
||||
selectedContentType = ContentType.VODS;
|
||||
currentLayout: LayoutView = 'category';
|
||||
vodDetails!: XtreamVodDetails | XtreamSerieDetails;
|
||||
settings = toSignal(
|
||||
this.storage.get(STORE_KEY.Settings)
|
||||
) as Signal<Settings>;
|
||||
settings = this.settingsStore.getSettings();
|
||||
isLoading = true;
|
||||
searchPhrase = this.portalStore.searchPhrase();
|
||||
contentId: number;
|
||||
|
||||
@@ -192,7 +192,8 @@
|
||||
"DESCRIPTION": "Description",
|
||||
"LANGUAGE": "Language",
|
||||
"SHOW_PROGRAM_DETAILS": "Show details about this program"
|
||||
}
|
||||
},
|
||||
"FETCH_SUCCESS": "EPG was fetched successfully"
|
||||
},
|
||||
"LANGUAGES": {
|
||||
"ENGLISH": "English",
|
||||
|
||||
Reference in new issue
Block a user