Compare commits
539
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9511e6e6a7 | ||
|
|
7f2b5ba47f | ||
|
|
478d19405d | ||
|
|
e363ab00bb | ||
|
|
dd9a6d5a06 | ||
|
|
fde05a0299 | ||
|
|
d3f8163580 | ||
|
|
07e4ff907f | ||
|
|
34874ac548 | ||
|
|
03e4c0207b | ||
|
|
b9faf87887 | ||
|
|
2257d26173 | ||
|
|
8f6d208e31 | ||
|
|
5ccea53131 | ||
|
|
da6cb09658 | ||
|
|
260949893c | ||
|
|
89b495dc90 | ||
|
|
74d6b32828 | ||
|
|
626af6876b | ||
|
|
2a05cc6e32 | ||
|
|
7a4c832156 | ||
|
|
011f16da9f | ||
|
|
79496e688b | ||
|
|
5c43ae47b1 | ||
|
|
c29788bc3f | ||
|
|
3501e7c8d5 | ||
|
|
867c88d8fc | ||
|
|
70fbd4c746 | ||
|
|
1cbd48cea0 | ||
|
|
c953176cee | ||
|
|
e0cef62969 | ||
|
|
9e56f6552f | ||
|
|
6a84c564d6 | ||
|
|
54be3761dd | ||
|
|
cf6b9cf29a | ||
|
|
464c1e2ea8 | ||
|
|
107e8cfded | ||
|
|
837f824660 | ||
|
|
223bdff8d6 | ||
|
|
578cdb1e14 | ||
|
|
848b88bd2d | ||
|
|
b85571b159 | ||
|
|
43e1cbd919 | ||
|
|
39b107eb0f | ||
|
|
0ee62dbc7d | ||
|
|
833bf3506a | ||
|
|
cd75046348 | ||
|
|
448d29546c | ||
|
|
f2c49bd0fd | ||
|
|
174c743cb7 | ||
|
|
2a9f23cce6 | ||
|
|
451c534062 | ||
|
|
e16cb30ab1 | ||
|
|
e0df454ac6 | ||
|
|
e79a03b522 | ||
|
|
1a09bb26d7 | ||
|
|
ffd3e3604c | ||
|
|
7e40a809ff | ||
|
|
cecf18a7b5 | ||
|
|
7df33425fa | ||
|
|
5dfaa1a7ad | ||
|
|
28a65e74bb | ||
|
|
4a66f0ae43 | ||
|
|
fb2466d32d | ||
|
|
beaaa62ed9 | ||
|
|
0b445f8cfd | ||
|
|
7e30444857 | ||
|
|
fa6a31b4fc | ||
|
|
b01ad9dbae | ||
|
|
d324967afa | ||
|
|
aff4fb0deb | ||
|
|
93afcd2f57 | ||
|
|
921a108684 | ||
|
|
a6fa93d44e | ||
|
|
a42234a7c3 | ||
|
|
7c5137a4af | ||
|
|
5a9d27e196 | ||
|
|
cd4a9c1d16 | ||
|
|
f6249d9fa4 | ||
|
|
e2ffa70529 | ||
|
|
3e07bc6136 | ||
|
|
d6bfc2fd05 | ||
|
|
35116c64cd | ||
|
|
037cee873f | ||
|
|
cd28afcd91 | ||
|
|
7457301d3e | ||
|
|
7b7d378df7 | ||
|
|
f6dcaf9108 | ||
|
|
6cc2f1de17 | ||
|
|
c6ee41484e | ||
|
|
36d38c740f | ||
|
|
0f795e4e2f | ||
|
|
583cbf7b14 | ||
|
|
27c701b936 | ||
|
|
6e2c19d354 | ||
|
|
4d83dc019c | ||
|
|
462057a4b1 | ||
|
|
a04c72788f | ||
|
|
f94a440b62 | ||
|
|
f80069bb97 | ||
|
|
c2769a08b4 | ||
|
|
e679fee940 | ||
|
|
2271d5497b | ||
|
|
f71b6527c0 | ||
|
|
20d2fe71cd | ||
|
|
1994f171d5 | ||
|
|
76f7c88375 | ||
|
|
3805b9e48c | ||
|
|
9267edbcc9 | ||
|
|
c4c164df6a | ||
|
|
b90463e3af | ||
|
|
1fb27e3cfa | ||
|
|
06f233e5bd | ||
|
|
9917774671 | ||
|
|
5be929da18 | ||
|
|
25f4fb22e5 | ||
|
|
b04b517f7b | ||
|
|
d756c0c7c0 | ||
|
|
20e5b8a11a | ||
|
|
5c8489cbed | ||
|
|
7cfa298c72 | ||
|
|
4b0faf4da1 | ||
|
|
07cbf9936b | ||
|
|
2138d6437c | ||
|
|
5b9601a57b | ||
|
|
aeda5050d3 | ||
|
|
ea46a7a5ca | ||
|
|
69a1e718df | ||
|
|
4a59dafe51 | ||
|
|
6b90da8982 | ||
|
|
1184dc565c | ||
|
|
d82ccf8fb5 | ||
|
|
4d83cc705f | ||
|
|
f80addacba | ||
|
|
18c2a816dc | ||
|
|
4f085c1950 | ||
|
|
dad0662fa6 | ||
|
|
dfdfa6f349 | ||
|
|
5fe3e97b31 | ||
|
|
da1cfab5f4 | ||
|
|
6d152e4b4a | ||
|
|
2ca722523b | ||
|
|
d9a3496bf5 | ||
|
|
c8f5b51d93 | ||
|
|
956734ce39 | ||
|
|
e44a391f00 | ||
|
|
b43b66dd35 | ||
|
|
140a663da4 | ||
|
|
876d79c11c | ||
|
|
57a4480c3f | ||
|
|
8f1b57eb88 | ||
|
|
70472ac84e | ||
|
|
6aab8f53b8 | ||
|
|
b30b458574 | ||
|
|
6fe6382485 | ||
|
|
100ae0adda | ||
|
|
f0d5200843 | ||
|
|
af36218cc2 | ||
|
|
eca62e8bec | ||
|
|
03d4ab8c72 | ||
|
|
57b590cd0f | ||
|
|
f0a5d89f73 | ||
|
|
17a77694a0 | ||
|
|
838c2a1661 | ||
|
|
375a306edc | ||
|
|
759052c725 | ||
|
|
dc112f0c7d | ||
|
|
c8ec87b01f | ||
|
|
fbb7a661fb | ||
|
|
37ceac5651 | ||
|
|
0953e258a5 | ||
|
|
fdbd8a07b6 | ||
|
|
5465c45a51 | ||
|
|
a0bef3568b | ||
|
|
f75bb25a1a | ||
|
|
e4ff825ae8 | ||
|
|
6c4f63ad91 | ||
|
|
c0a14ba81c | ||
|
|
c063720169 | ||
|
|
5d0f40978d | ||
|
|
063f463951 | ||
|
|
e7817b1460 | ||
|
|
7d6faee27b | ||
|
|
7bba422880 | ||
|
|
db6ae27384 | ||
|
|
4b9fc5004f | ||
|
|
f40eaef898 | ||
|
|
91e85cc9c1 | ||
|
|
2c44efb971 | ||
|
|
c2b7be66af | ||
|
|
8b911332a6 | ||
|
|
4130f7316c | ||
|
|
3f6eb5a121 | ||
|
|
1209c54eb9 | ||
|
|
94db4bf679 | ||
|
|
2977590a14 | ||
|
|
b4c168e85e | ||
|
|
55b7a35689 | ||
|
|
a24592a8c4 | ||
|
|
9b60ff0863 | ||
|
|
efdf0bb6d4 | ||
|
|
39ca27cb3d | ||
|
|
9e2f7b7815 | ||
|
|
101d46e283 | ||
|
|
521e4eac41 | ||
|
|
894fc284b2 | ||
|
|
a8cf22e43e | ||
|
|
4c9c047530 | ||
|
|
912f79097d | ||
|
|
8aa55fdfce | ||
|
|
8dc1cab222 | ||
|
|
961fe8bbf2 | ||
|
|
75f991d670 | ||
|
|
e3c981004b | ||
|
|
befaa037e2 | ||
|
|
5e0fb31069 | ||
|
|
7d83e66ba6 | ||
|
|
391528cd94 | ||
|
|
b737775f9a | ||
|
|
728c5130b5 | ||
|
|
e4253276e0 | ||
|
|
1fc55bc693 | ||
|
|
4ad22e402f | ||
|
|
ec99d5976d | ||
|
|
59f11f1a1a | ||
|
|
694f25f8b3 | ||
|
|
5947555e86 | ||
|
|
fb63116b36 | ||
|
|
56a58d7a84 | ||
|
|
6f66909957 | ||
|
|
01090f62e6 | ||
|
|
e4e4f68eb4 | ||
|
|
8488fe5d3d | ||
|
|
f06ef5262a | ||
|
|
ae6bcc4933 | ||
|
|
b83fe53ef1 | ||
|
|
d50f2ace07 | ||
|
|
23684f607a | ||
|
|
fa20c5e01e | ||
|
|
53bd745678 | ||
|
|
f3e5a4e7d8 | ||
|
|
0b29bb32b1 | ||
|
|
d9a7615cf6 | ||
|
|
50f2cb7a33 | ||
|
|
b1b2c2a1e0 | ||
|
|
d842cd57f6 | ||
|
|
4f393d7b06 | ||
|
|
46f7289db8 | ||
|
|
80ccbbf299 | ||
|
|
3765894cb7 | ||
|
|
a8b658a5ea | ||
|
|
0e3c32bd83 | ||
|
|
9dd4a85bf9 | ||
|
|
a0a047ba18 | ||
|
|
687a4f4f10 | ||
|
|
b91ab5d898 | ||
|
|
256042947d | ||
|
|
85029cbbcd | ||
|
|
b5d679212d | ||
|
|
36e86587ef | ||
|
|
f41fa669be | ||
|
|
f62e841af4 | ||
|
|
109960c457 | ||
|
|
6858103be5 | ||
|
|
f7f1a0493b | ||
|
|
6c18648fd7 | ||
|
|
ab3afcfad0 | ||
|
|
5734356d29 | ||
|
|
beee038be3 | ||
|
|
a5c8e2b401 | ||
|
|
c8113bdf25 | ||
|
|
9466cf7626 | ||
|
|
43fcf9e63a | ||
|
|
242a7ae382 | ||
|
|
6dc526a817 | ||
|
|
182c02b865 | ||
|
|
7c1f0d6dbd | ||
|
|
2c97c49763 | ||
|
|
f5038c2b66 | ||
|
|
85eb7623da | ||
|
|
a87ec2d75d | ||
|
|
ef6c8b0816 | ||
|
|
dc3578660f | ||
|
|
ce958bb7bb | ||
|
|
33a8b29a27 | ||
|
|
48a504735c | ||
|
|
0b64e97df6 | ||
|
|
5d89f5d0a4 | ||
|
|
50a6ed4cea | ||
|
|
66b8c8aa0e | ||
|
|
398a3c041a | ||
|
|
6ce7265427 | ||
|
|
b4fe38d4ae | ||
|
|
e19b639527 | ||
|
|
a6d5df3ca6 | ||
|
|
202ae33e37 | ||
|
|
c46b3787d8 | ||
|
|
706a2d14a2 | ||
|
|
11f99216a3 | ||
|
|
b9a7ad2f5a | ||
|
|
07e7e5fe66 | ||
|
|
4a19d046e4 | ||
|
|
c1d6ddcc57 | ||
|
|
35eb200aee | ||
|
|
19af303d76 | ||
|
|
da20393a39 | ||
|
|
d78110f6f1 | ||
|
|
c1bedb661c | ||
|
|
d31d6f20cf | ||
|
|
7469559bb3 | ||
|
|
af5dc0968b | ||
|
|
6e7880386b | ||
|
|
8945a87f9b | ||
|
|
e305222141 | ||
|
|
2e2523c380 | ||
|
|
b461631be9 | ||
|
|
19fc068a73 | ||
|
|
e5f15df196 | ||
|
|
6ac2384cbc | ||
|
|
cd6673da82 | ||
|
|
8113827802 | ||
|
|
4e56117e0e | ||
|
|
7702999b9a | ||
|
|
14a707a4e2 | ||
|
|
340ab61a26 | ||
|
|
d91d991251 | ||
|
|
3ce341eee5 | ||
|
|
476fe991b6 | ||
|
|
39df3504fc | ||
|
|
60bb369d0c | ||
|
|
aae704f3a5 | ||
|
|
a45583d77a | ||
|
|
923b36604c | ||
|
|
b21d16b0f1 | ||
|
|
a5aaceeee5 | ||
|
|
e52d45fcf8 | ||
|
|
21d39bc26f | ||
|
|
233a1c228a | ||
|
|
56988be57b | ||
|
|
aded03d962 | ||
|
|
2119c88c97 | ||
|
|
a5d83a970a | ||
|
|
986785d863 | ||
|
|
087901d177 | ||
|
|
70c4036dc9 | ||
|
|
955add1efd | ||
|
|
99cd01f73b | ||
|
|
ef29e8c5a1 | ||
|
|
3b4c993530 | ||
|
|
bcc58bd668 | ||
|
|
6957a76156 | ||
|
|
4bafc316cc | ||
|
|
35817f09ac | ||
|
|
f4520a5520 | ||
|
|
3a906816fc | ||
|
|
707292c50f | ||
|
|
71e9ea867a | ||
|
|
c490832f66 | ||
|
|
e00568cc23 | ||
|
|
356e0f101a | ||
|
|
1f6e843a26 | ||
|
|
9587692486 | ||
|
|
f8c4f44216 | ||
|
|
d55ba235bf | ||
|
|
60b479e330 | ||
|
|
b866d07911 | ||
|
|
93db79f8c4 | ||
|
|
a15854d0ad | ||
|
|
c743d07425 | ||
|
|
8c3b8e81ca | ||
|
|
49050a57d2 | ||
|
|
49c53c5c96 | ||
|
|
1510c56e69 | ||
|
|
3ec610d65f | ||
|
|
69f9b6f137 | ||
|
|
08837bda80 | ||
|
|
9089e2ee04 | ||
|
|
abed22b560 | ||
|
|
e0f9ab4b88 | ||
|
|
231a214223 | ||
|
|
82bfa8019e | ||
|
|
d9bbe4df1b | ||
|
|
e0aa44d41b | ||
|
|
3d99c2593d | ||
|
|
d6dfc1edaa | ||
|
|
7d5cd229d4 | ||
|
|
cd0219c5c3 | ||
|
|
4cf8b83de4 | ||
|
|
6923b25177 | ||
|
|
5dce905b8e | ||
|
|
46c26b5ea7 | ||
|
|
7fffc8cf63 | ||
|
|
18deff0b83 | ||
|
|
16007a888e | ||
|
|
7eb1227ba4 | ||
|
|
1d1d5bf9bc | ||
|
|
45c04366c9 | ||
|
|
60b3bc92f4 | ||
|
|
12234c3e21 | ||
|
|
d37ce2d38a | ||
|
|
6f49233864 | ||
|
|
a67a6047c1 | ||
|
|
33f67b88f0 | ||
|
|
b88deaafe5 | ||
|
|
83fc3081d8 | ||
|
|
15d4b0f82b | ||
|
|
88fac0de04 | ||
|
|
4805d0d40f | ||
|
|
ef3b941a39 | ||
|
|
a59f71039c | ||
|
|
1ad42fffb1 | ||
|
|
2ce8db9e01 | ||
|
|
c409fd8b47 | ||
|
|
907b8074f1 | ||
|
|
adbd0bcec0 | ||
|
|
2c4379886a | ||
|
|
caef4a139e | ||
|
|
dcbe4837bf | ||
|
|
5e530b9301 | ||
|
|
2a28bf68bf | ||
|
|
f39eac97c0 | ||
|
|
9fd6589831 | ||
|
|
e2a516f5e8 | ||
|
|
64502315a3 | ||
|
|
56bc58fce9 | ||
|
|
0330b9326d | ||
|
|
6708d6b4d7 | ||
|
|
c18be5559b | ||
|
|
18ed20e203 | ||
|
|
965c7d0eac | ||
|
|
545bf1b775 | ||
|
|
bb299d4ee7 | ||
|
|
0e6c7d2bc3 | ||
|
|
576f0cd7e7 | ||
|
|
9471cb55dd | ||
|
|
3a84af1626 | ||
|
|
3d3bb64844 | ||
|
|
8fc1f36638 | ||
|
|
1823a5bae5 | ||
|
|
fc871e6f74 | ||
|
|
24780cbe84 | ||
|
|
c6ed258021 | ||
|
|
7586647b73 | ||
|
|
d91e945124 | ||
|
|
9dabffbac1 | ||
|
|
d310b5c09d | ||
|
|
ba48b3a676 | ||
|
|
d8a51b5d6d | ||
|
|
97674cff89 | ||
|
|
4820615308 | ||
|
|
1ddf27ce88 | ||
|
|
cd98a89acd | ||
|
|
a2a6afc3e3 | ||
|
|
dfaba8c7b0 | ||
|
|
5d11a6b46f | ||
|
|
b95a89b11f | ||
|
|
948b3735bd | ||
|
|
5ecf271773 | ||
|
|
b287c0d6ec | ||
|
|
b667659c05 | ||
|
|
22d3025e8e | ||
|
|
8f5b181372 | ||
|
|
f5060522aa | ||
|
|
14a88bd225 | ||
|
|
0550c60a78 | ||
|
|
d3bdcf9bc4 | ||
|
|
714f68a887 | ||
|
|
17bed524f2 | ||
|
|
c3fe263978 | ||
|
|
5291832e6c | ||
|
|
b39dd693f0 | ||
|
|
46bf9ef990 | ||
|
|
bc845b1327 | ||
|
|
3ab8e5bc3a | ||
|
|
e8bc051f73 | ||
|
|
b008fcfd85 | ||
|
|
547db5fb51 | ||
|
|
58fae1b0cc | ||
|
|
694b6bbd91 | ||
|
|
e0f8b7d7ae | ||
|
|
b16215fcd6 | ||
|
|
85f2c658aa | ||
|
|
78356314e6 | ||
|
|
b00a25bbee | ||
|
|
4d77576be2 | ||
|
|
a90348740d | ||
|
|
8081845ef1 | ||
|
|
d014eb4274 | ||
|
|
8c9cf7b6f2 | ||
|
|
5d9c8d4f7d | ||
|
|
82de3136cd | ||
|
|
b1cd324f9c | ||
|
|
5caf8f7f98 | ||
|
|
245c4ec359 | ||
|
|
6414471ace | ||
|
|
8b0b927a5c | ||
|
|
e5962699a4 | ||
|
|
27504e42bc | ||
|
|
deb0ac49b5 | ||
|
|
225b95449c | ||
|
|
cb43c28d00 | ||
|
|
0a75136223 | ||
|
|
efc710749e | ||
|
|
8f241f49fc | ||
|
|
20d224fcfd | ||
|
|
b3fda4e88d | ||
|
|
560cb826b3 | ||
|
|
b038a58fa2 | ||
|
|
c28e201e47 | ||
|
|
b84bb6b437 | ||
|
|
641b8bcd10 | ||
|
|
4b08ed5381 | ||
|
|
5486dcdcab | ||
|
|
77b32b0f09 | ||
|
|
2c6c08becf | ||
|
|
99bc19cf26 | ||
|
|
a7661c8498 | ||
|
|
d951035183 | ||
|
|
097c60169c | ||
|
|
d64d8b0454 | ||
|
|
7486304ed9 | ||
|
|
c62cc98c9f | ||
|
|
22a13cb1b3 | ||
|
|
5e573461f3 | ||
|
|
76c596a7d8 | ||
|
|
f945f16d97 | ||
|
|
797d4005e2 | ||
|
|
55903430ae | ||
|
|
f929dc92d1 | ||
|
|
2ad27c2be0 | ||
|
|
df2db5caf7 | ||
|
|
5978e8ecb1 | ||
|
|
a540efc2e1 | ||
|
|
1938cef6ae | ||
|
|
b23d798aff | ||
|
|
ebad7664b0 | ||
|
|
a9c93ff498 | ||
|
|
8277894f7b | ||
|
|
0d66f752b6 |
@@ -3,16 +3,11 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"jetbrains.resharper.globaltools": {
|
"jetbrains.resharper.globaltools": {
|
||||||
"version": "2023.1.1",
|
"version": "2025.1.4",
|
||||||
"commands": [
|
"commands": [
|
||||||
"jb"
|
"jb"
|
||||||
]
|
],
|
||||||
},
|
"rollForward": false
|
||||||
"swashbuckle.aspnetcore.cli": {
|
|
||||||
"version": "5.6.2",
|
|
||||||
"commands": [
|
|
||||||
"swagger"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-1
@@ -2,7 +2,7 @@
|
|||||||
[*]
|
[*]
|
||||||
charset=utf-8
|
charset=utf-8
|
||||||
end_of_line=lf
|
end_of_line=lf
|
||||||
trim_trailing_whitespace=false
|
trim_trailing_whitespace=true
|
||||||
insert_final_newline=false
|
insert_final_newline=false
|
||||||
indent_style=space
|
indent_style=space
|
||||||
indent_size=4
|
indent_size=4
|
||||||
@@ -83,3 +83,8 @@ tab_width=4
|
|||||||
[*.yml]
|
[*.yml]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
# disable CA1848: Use the LoggerMessage delegates`
|
||||||
|
dotnet_diagnostic.ca1848.severity = none
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
*.cs text diff=csharp
|
||||||
|
*.cshtml text diff=html
|
||||||
|
*.csx text diff=csharp
|
||||||
|
*.sln text eol=crlf
|
||||||
|
*.csproj text eol=crlf
|
||||||
@@ -33,33 +33,23 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: macos-11
|
- os: macos-14
|
||||||
kind: macOS
|
kind: macOS
|
||||||
target: osx-x64
|
target: osx-x64
|
||||||
- os: macos-11
|
- os: macos-14
|
||||||
kind: macOS
|
kind: macOS
|
||||||
target: osx-arm64
|
target: osx-arm64
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: 9.0.203
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
|
|
||||||
- name: Cache NPM dependencies
|
|
||||||
uses: bahmutov/npm-install@v1.8.28
|
|
||||||
with:
|
|
||||||
working-directory: ErsatzTV/client-app
|
|
||||||
|
|
||||||
- name: Clean
|
- name: Clean
|
||||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
@@ -68,7 +58,7 @@ jobs:
|
|||||||
run: dotnet restore -r "${{ matrix.target}}"
|
run: dotnet restore -r "${{ matrix.target}}"
|
||||||
|
|
||||||
- name: Import Code-Signing Certificates
|
- name: Import Code-Signing Certificates
|
||||||
uses: Apple-Actions/import-codesign-certs@v1
|
uses: Apple-Actions/import-codesign-certs@v2
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.apple_developer_certificate_p12_base64 }}
|
p12-file-base64: ${{ secrets.apple_developer_certificate_p12_base64 }}
|
||||||
p12-password: ${{ secrets.apple_developer_certificate_password }}
|
p12-password: ${{ secrets.apple_developer_certificate_password }}
|
||||||
@@ -83,8 +73,8 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sed -i '' '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
sed -i '' '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||||
|
|
||||||
- name: Bundle
|
- name: Bundle
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -112,18 +102,15 @@ jobs:
|
|||||||
--hide-extension "ErsatzTV.app" \
|
--hide-extension "ErsatzTV.app" \
|
||||||
--app-drop-link 600 185 \
|
--app-drop-link 600 185 \
|
||||||
--skip-jenkins \
|
--skip-jenkins \
|
||||||
|
--no-internet-enable \
|
||||||
"ErsatzTV.dmg" \
|
"ErsatzTV.dmg" \
|
||||||
"ErsatzTV.app/"
|
"ErsatzTV.app/"
|
||||||
|
|
||||||
- name: Notarize
|
- name: Notarize
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
brew tap mitchellh/gon
|
xcrun notarytool submit ErsatzTV.dmg --apple-id "${{ secrets.ac_username }}" --password "${{ secrets.ac_password }}" --team-id 32MB98Q32R --wait
|
||||||
brew install mitchellh/gon/gon
|
xcrun stapler staple ErsatzTV.dmg
|
||||||
gon -log-level=debug -log-json ./gon.json
|
|
||||||
env:
|
|
||||||
AC_USERNAME: ${{ secrets.ac_username }}
|
|
||||||
AC_PASSWORD: ${{ secrets.ac_password }}
|
|
||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -145,7 +132,7 @@ jobs:
|
|||||||
- name: Publish
|
- name: Publish
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: false
|
||||||
tag_name: ${{ inputs.release_tag }}
|
tag_name: ${{ inputs.release_tag }}
|
||||||
files: |
|
files: |
|
||||||
${{ env.RELEASE_NAME }}.dmg
|
${{ env.RELEASE_NAME }}.dmg
|
||||||
@@ -163,8 +150,11 @@ jobs:
|
|||||||
target: linux-x64
|
target: linux-x64
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
kind: linux
|
kind: linux
|
||||||
target: linux-arm
|
target: linux-musl-x64
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
|
kind: linux
|
||||||
|
target: linux-arm
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
kind: linux
|
kind: linux
|
||||||
target: linux-arm64
|
target: linux-arm64
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
@@ -172,30 +162,14 @@ jobs:
|
|||||||
target: win-x64
|
target: win-x64
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: 9.0.203
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
|
|
||||||
- name: Setup Rust
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
if: ${{ matrix.kind == 'windows' }}
|
|
||||||
|
|
||||||
- name: Cache NPM dependencies
|
|
||||||
uses: bahmutov/npm-install@v1.8.28
|
|
||||||
with:
|
|
||||||
working-directory: ErsatzTV/client-app
|
|
||||||
|
|
||||||
- name: Clean
|
- name: Clean
|
||||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
@@ -208,7 +182,7 @@ jobs:
|
|||||||
id: downloadffmpeg
|
id: downloadffmpeg
|
||||||
name: Download ffmpeg
|
name: Download ffmpeg
|
||||||
with:
|
with:
|
||||||
url: "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.7z"
|
url: "https://github.com/ErsatzTV/ErsatzTV-ffmpeg/releases/download/7.1.1/ffmpeg-n7.1.1-56-gc2184b65d2-win64-gpl-7.1.zip"
|
||||||
target: ffmpeg/
|
target: ffmpeg/
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -220,8 +194,11 @@ jobs:
|
|||||||
|
|
||||||
# Build everything
|
# Build everything
|
||||||
sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o "scanner" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o "main" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||||
|
mkdir "$release_name"
|
||||||
|
mv scanner/* "$release_name/"
|
||||||
|
mv main/* "$release_name/"
|
||||||
|
|
||||||
# Build Windows launcher
|
# Build Windows launcher
|
||||||
if [ "${{ matrix.kind }}" == "windows" ]; then
|
if [ "${{ matrix.kind }}" == "windows" ]; then
|
||||||
@@ -245,9 +222,6 @@ jobs:
|
|||||||
|
|
||||||
# Delete output directory
|
# Delete output directory
|
||||||
rm -r "$release_name"
|
rm -r "$release_name"
|
||||||
env:
|
|
||||||
AC_USERNAME: ${{ secrets.ac_username }}
|
|
||||||
AC_PASSWORD: ${{ secrets.ac_password }}
|
|
||||||
|
|
||||||
- name: Delete old release assets
|
- name: Delete old release assets
|
||||||
uses: mknejp/delete-release-assets@v1
|
uses: mknejp/delete-release-assets@v1
|
||||||
@@ -259,10 +233,11 @@ jobs:
|
|||||||
assets: |
|
assets: |
|
||||||
*${{ matrix.target }}.zip
|
*${{ matrix.target }}.zip
|
||||||
*${{ matrix.target }}.tar.gz
|
*${{ matrix.target }}.tar.gz
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: false
|
||||||
tag_name: ${{ inputs.release_tag }}
|
tag_name: ${{ inputs.release_tag }}
|
||||||
files: |
|
files: |
|
||||||
${{ env.RELEASE_NAME }}.zip
|
${{ env.RELEASE_NAME }}.zip
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Extract Docker Tag
|
- name: Extract Docker Tag
|
||||||
@@ -19,14 +19,14 @@ jobs:
|
|||||||
tag=$(git describe --tags --abbrev=0)
|
tag=$(git describe --tags --abbrev=0)
|
||||||
tag2="${tag:1}"
|
tag2="${tag:1}"
|
||||||
short=$(git rev-parse --short HEAD)
|
short=$(git rev-parse --short HEAD)
|
||||||
final="${tag2/beta/$short}"
|
final="${tag2}-${short}"
|
||||||
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
||||||
- name: Extract Artifacts Version
|
- name: Extract Artifacts Version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
tag=$(git describe --tags --abbrev=0)
|
tag=$(git describe --tags --abbrev=0)
|
||||||
short=$(git rev-parse --short HEAD)
|
short=$(git rev-parse --short HEAD)
|
||||||
final="${tag/beta/$short}"
|
final="${tag}-${short}"
|
||||||
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
|
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
|
||||||
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
|
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
|
||||||
outputs:
|
outputs:
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
|
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
|
||||||
info_version: ${{ env.INFO_VERSION }}
|
info_version: ${{ env.INFO_VERSION }}
|
||||||
build_and_upload:
|
build_and_upload:
|
||||||
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
|
uses: ersatztv/ersatztv/.github/workflows/artifacts.yml@main
|
||||||
needs: calculate_version
|
needs: calculate_version
|
||||||
with:
|
with:
|
||||||
release_tag: develop
|
release_tag: develop
|
||||||
@@ -46,8 +46,8 @@ jobs:
|
|||||||
ac_username: ${{ secrets.AC_USERNAME }}
|
ac_username: ${{ secrets.AC_USERNAME }}
|
||||||
ac_password: ${{ secrets.AC_PASSWORD }}
|
ac_password: ${{ secrets.AC_PASSWORD }}
|
||||||
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build_and_push:
|
build_images:
|
||||||
uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
|
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
|
||||||
needs: calculate_version
|
needs: calculate_version
|
||||||
with:
|
with:
|
||||||
base_version: develop
|
base_version: develop
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build & Publish to Docker Hub
|
name: Build & Publish to Docker Hub
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -20,93 +20,122 @@ on:
|
|||||||
docker_hub_access_token:
|
docker_hub_access_token:
|
||||||
required: true
|
required: true
|
||||||
jobs:
|
jobs:
|
||||||
build_and_push:
|
build_images:
|
||||||
name: Build & Publish
|
name: Build ${{ matrix.name }} image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
if: contains(github.event.head_commit.message, '[no build]') == false
|
if: contains(github.event.head_commit.message, '[no build]') == false
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: base
|
- name: amd64
|
||||||
|
os: ubuntu-latest
|
||||||
path: ''
|
path: ''
|
||||||
suffix: ''
|
suffix: '-amd64'
|
||||||
qemu: false
|
platform: 'linux/amd64'
|
||||||
- name: nvidia
|
|
||||||
path: 'nvidia/'
|
|
||||||
suffix: '-nvidia'
|
|
||||||
qemu: false
|
|
||||||
- name: vaapi
|
|
||||||
path: 'vaapi/'
|
|
||||||
suffix: '-vaapi'
|
|
||||||
qemu: false
|
|
||||||
- name: arm32v7
|
- name: arm32v7
|
||||||
|
os: ubuntu-latest
|
||||||
path: 'arm32v7/'
|
path: 'arm32v7/'
|
||||||
suffix: '-arm'
|
suffix: '-arm'
|
||||||
qemu: true
|
platform: 'linux/arm/v7'
|
||||||
- name: arm64
|
- name: arm64
|
||||||
|
os: ubuntu-24.04-arm
|
||||||
path: 'arm64/'
|
path: 'arm64/'
|
||||||
suffix: '-arm64'
|
suffix: '-arm64'
|
||||||
qemu: true
|
platform: 'linux/arm64'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
if: ${{ matrix.name == 'arm32v7' }}
|
||||||
if: ${{ matrix.qemu == true }}
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
id: docker-buildx
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.docker_hub_username }}
|
username: ${{ secrets.docker_hub_username }}
|
||||||
password: ${{ secrets.docker_hub_access_token }}
|
password: ${{ secrets.docker_hub_access_token }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Log in to the Container registry
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
registry: ghcr.io
|
||||||
context: .
|
username: ${{ github.actor }}
|
||||||
file: ./docker/${{ matrix.path }}Dockerfile
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
|
||||||
tags: |
|
|
||||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
|
||||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
|
||||||
if: ${{ matrix.name != 'arm64' && matrix.name != 'arm32v7' }}
|
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push by digest
|
||||||
uses: docker/build-push-action@v3
|
id: build
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/${{ matrix.path }}Dockerfile
|
file: ./docker/${{ matrix.path }}Dockerfile
|
||||||
push: true
|
push: true
|
||||||
platforms: 'linux/arm64'
|
provenance: false
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
build-args: |
|
build-args: |
|
||||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||||
tags: |
|
outputs: |
|
||||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
type=image,name=jasongdove/ersatztv,name-canonical=true,push-by-digest=true
|
||||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
type=image,name=ghcr.io/ersatztv/ersatztv,name-canonical=true,push-by-digest=true
|
||||||
if: ${{ matrix.name == 'arm64' }}
|
|
||||||
|
|
||||||
- name: Build and push
|
- name: Save digest to artifact
|
||||||
uses: docker/build-push-action@v3
|
run: echo ${{ steps.build.outputs.digest }} > digest.txt
|
||||||
|
|
||||||
|
- name: Upload digest artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
name: digest-${{ matrix.name }}
|
||||||
context: .
|
path: digest.txt
|
||||||
file: ./docker/${{ matrix.path }}Dockerfile
|
|
||||||
push: true
|
merge_manifests:
|
||||||
platforms: 'linux/arm/v7'
|
name: Merge Manifests
|
||||||
build-args: |
|
runs-on: ubuntu-latest
|
||||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
needs: build_images
|
||||||
tags: |
|
steps:
|
||||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
- name: Login to DockerHub
|
||||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
uses: docker/login-action@v3
|
||||||
if: ${{ matrix.name == 'arm32v7' }}
|
with:
|
||||||
|
username: ${{ secrets.docker_hub_username }}
|
||||||
|
password: ${{ secrets.docker_hub_access_token }}
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Download all digest artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: digests/
|
||||||
|
|
||||||
|
- name: Read digests from artifacts
|
||||||
|
id: digests
|
||||||
|
run: |
|
||||||
|
AMD64_HASH=$(cat digests/digest-amd64/digest.txt)
|
||||||
|
ARM32V7_HASH=$(cat digests/digest-arm32v7/digest.txt)
|
||||||
|
ARM64_HASH=$(cat digests/digest-arm64/digest.txt)
|
||||||
|
|
||||||
|
DOCKER_HUB_DIGESTS="jasongdove/ersatztv@${AMD64_HASH} jasongdove/ersatztv@${ARM64_HASH} jasongdove/ersatztv@${ARM32V7_HASH}"
|
||||||
|
GHCR_DIGESTS="ghcr.io/ersatztv/ersatztv@${AMD64_HASH} ghcr.io/ersatztv/ersatztv@${ARM64_HASH} ghcr.io/ersatztv/ersatztv@${ARM32V7_HASH}"
|
||||||
|
|
||||||
|
echo "docker_hub_digests=${DOCKER_HUB_DIGESTS}" >> $GITHUB_OUTPUT
|
||||||
|
echo "ghcr_digests=${GHCR_DIGESTS}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create and push manifests
|
||||||
|
run: |
|
||||||
|
docker manifest create jasongdove/ersatztv:${{ inputs.base_version }} ${{ steps.digests.outputs.docker_hub_digests }}
|
||||||
|
docker manifest push jasongdove/ersatztv:${{ inputs.base_version }}
|
||||||
|
docker manifest create jasongdove/ersatztv:${{ inputs.tag_version }} ${{ steps.digests.outputs.docker_hub_digests }}
|
||||||
|
docker manifest push jasongdove/ersatztv:${{ inputs.tag_version }}
|
||||||
|
|
||||||
|
docker manifest create ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }} ${{ steps.digests.outputs.ghcr_digests }}
|
||||||
|
docker manifest push ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}
|
||||||
|
docker manifest create ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }} ${{ steps.digests.outputs.ghcr_digests }}
|
||||||
|
docker manifest push ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
name: Publish docs via GitHub Pages
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- docs/**
|
|
||||||
- mkdocs.yml
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Deploy docs
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout master
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Deploy docs
|
|
||||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CUSTOM_DOMAIN: ersatztv.org
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: 'Close stale issues'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 1 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
ascending: true
|
||||||
|
days-before-stale: 120
|
||||||
|
days-before-pr-stale: -1
|
||||||
|
days-before-close: 21
|
||||||
|
days-before-pr-close: -1
|
||||||
|
operations-per-run: 500
|
||||||
|
exempt-issue-labels: 'regression,security,roadmap,future,feature,enhancement,confirmed'
|
||||||
|
stale-issue-label: 'stale'
|
||||||
|
stale-issue-message: |-
|
||||||
|
This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
|
||||||
|
|
||||||
|
If you have any questions you can use one of several ways to [contact us](https://ersatztv.org).
|
||||||
|
close-issue-message: |-
|
||||||
|
This issue was closed due to inactivity.
|
||||||
+27
-21
@@ -6,17 +6,12 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: 9.0.203
|
||||||
|
|
||||||
- name: Setup Rust
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
|
|
||||||
- name: Clean
|
- name: Clean
|
||||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
@@ -31,50 +26,61 @@ jobs:
|
|||||||
run: dotnet build --configuration Release --no-restore
|
run: dotnet build --configuration Release --no-restore
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test --no-restore --verbosity normal
|
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal
|
||||||
|
|
||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
run: |
|
run: |
|
||||||
cd ErsatzTV-Windows
|
cd ErsatzTV-Windows
|
||||||
cargo build --release --all-features
|
cargo build --release --all-features
|
||||||
build_and_test_linux:
|
build_and_test_linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: linux-x64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: linux-musl-x64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: linux-arm
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
|
target: linux-arm64
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: 9.0.203
|
||||||
|
|
||||||
- name: Clean
|
- name: Clean
|
||||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore -p:RestoreEnablePackagePruning=true -r "${{ matrix.target }}"
|
||||||
|
|
||||||
- name: Prep project file
|
- name: Prep project file
|
||||||
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build --configuration Release --no-restore
|
run: dotnet build ErsatzTV/ErsatzTV.csproj --runtime "${{ matrix.target }}" --configuration Release --no-restore && dotnet build --configuration Release --no-restore
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test --no-restore --verbosity normal
|
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal
|
||||||
build_and_test_mac:
|
build_and_test_mac:
|
||||||
runs-on: macos-11
|
runs-on: macos-14
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: 9.0.203
|
||||||
|
|
||||||
- name: Clean
|
- name: Clean
|
||||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
@@ -89,4 +95,4 @@ jobs:
|
|||||||
run: dotnet build --configuration Release --no-restore
|
run: dotnet build --configuration Release --no-restore
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test --no-restore --verbosity normal
|
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Extract Docker Tag
|
- name: Extract Docker Tag
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
tag=$(git describe --tags --abbrev=0)
|
tag=$(git describe --tags --abbrev=0)
|
||||||
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
||||||
echo "DOCKER_TAG=${tag/-beta/}" >> $GITHUB_ENV
|
echo "DOCKER_TAG=${tag}" >> $GITHUB_ENV
|
||||||
- name: Extract Artifacts Version
|
- name: Extract Artifacts Version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
|
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
|
||||||
info_version: ${{ env.INFO_VERSION }}
|
info_version: ${{ env.INFO_VERSION }}
|
||||||
build_and_upload:
|
build_and_upload:
|
||||||
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
|
uses: ersatztv/ersatztv/.github/workflows/artifacts.yml@main
|
||||||
needs: calculate_version
|
needs: calculate_version
|
||||||
with:
|
with:
|
||||||
release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
|
release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
|
||||||
@@ -41,8 +41,8 @@ jobs:
|
|||||||
ac_username: ${{ secrets.AC_USERNAME }}
|
ac_username: ${{ secrets.AC_USERNAME }}
|
||||||
ac_password: ${{ secrets.AC_PASSWORD }}
|
ac_password: ${{ secrets.AC_PASSWORD }}
|
||||||
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build_and_push:
|
build_images:
|
||||||
uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
|
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
|
||||||
needs: calculate_version
|
needs: calculate_version
|
||||||
with:
|
with:
|
||||||
base_version: latest
|
base_version: latest
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
name: Lint VueJS Files on PR Request
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
jobs:
|
|
||||||
vue-lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Checkout the current repo
|
|
||||||
- name: Checkout current repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
# Setup NodeJS version 14
|
|
||||||
- name: Setup NodeJS V14.x.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
# CD into the current client directory and lint and build the client
|
|
||||||
- name: Lint and Build the client
|
|
||||||
run: |
|
|
||||||
cd ./ErsatzTV/client-app/
|
|
||||||
npm ci --no-optional
|
|
||||||
npm run lint
|
|
||||||
npm run build --if-present
|
|
||||||
@@ -40,6 +40,7 @@ msbuild.wrn
|
|||||||
core
|
core
|
||||||
|
|
||||||
scripts/generate-api-sdk/swagger.json
|
scripts/generate-api-sdk/swagger.json
|
||||||
|
scripts/download-test-content.sh
|
||||||
|
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
[submodule "ErsatzTV-macOS"]
|
[submodule "ErsatzTV-macOS"]
|
||||||
path = ErsatzTV-macOS
|
path = ErsatzTV-macOS
|
||||||
url = git@github.com:jasongdove/ErsatzTV-macOS.git
|
url = git@github.com:ErsatzTV/ErsatzTV-macOS.git
|
||||||
|
|||||||
+864
-120
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<InformationalVersion>develop</InformationalVersion>
|
<InformationalVersion>develop</InformationalVersion>
|
||||||
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
Generated
+7
@@ -173,6 +173,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"process_path",
|
"process_path",
|
||||||
"special-folder",
|
"special-folder",
|
||||||
|
"static_vcruntime",
|
||||||
"tray-item",
|
"tray-item",
|
||||||
"windows",
|
"windows",
|
||||||
"windres",
|
"windres",
|
||||||
@@ -769,6 +770,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_vcruntime"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "954e3e877803def9dc46075bf4060147c55cd70db97873077232eae0269dc89b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.24.1"
|
version = "0.24.1"
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ features = [
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
windres = "*"
|
windres = "*"
|
||||||
|
static_vcruntime = "2.0"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use windres::Build;
|
use windres::Build;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
static_vcruntime::metabuild();
|
||||||
Build::new().compile("ersatztv_windows.rc").unwrap();
|
Build::new().compile("ersatztv_windows.rc").unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
use special_folder::SpecialFolder;
|
use special_folder::SpecialFolder;
|
||||||
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
use std::process::Child;
|
use std::process::Child;
|
||||||
@@ -21,11 +22,16 @@ fn main() {
|
|||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
tray.add_menu_item("Launch Web UI", || {
|
tray.add_menu_item("Launch Web UI", || {
|
||||||
|
let ui_port = env::var("ETV_UI_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|val| val.parse::<u16>().ok())
|
||||||
|
.unwrap_or(8409);
|
||||||
|
|
||||||
let _ = Command::new("cmd")
|
let _ = Command::new("cmd")
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
.arg("/C")
|
.arg("/C")
|
||||||
.arg("start")
|
.arg("start")
|
||||||
.arg("http://localhost:8409")
|
.arg(format!("http://localhost:{}", ui_port))
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
@@ -43,10 +49,7 @@ fn main() {
|
|||||||
None => {}
|
None => {}
|
||||||
Some(folder) => {
|
Some(folder) => {
|
||||||
fs::create_dir_all(folder).unwrap();
|
fs::create_dir_all(folder).unwrap();
|
||||||
let _ = Command::new("cmd")
|
let _ = Command::new("explorer.exe")
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
|
||||||
.arg("/C")
|
|
||||||
.arg("start")
|
|
||||||
.arg(folder)
|
.arg(folder)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
|
|||||||
+1
-1
Submodule ErsatzTV-macOS updated: 2f3ee16f11...d4dd985fd6
@@ -0,0 +1,5 @@
|
|||||||
|
[*.cs]
|
||||||
|
# disable CA1711: Identifiers should not have incorrect suffix
|
||||||
|
dotnet_diagnostic.ca1711.severity = none
|
||||||
|
# disable CA1848: Use the LoggerMessage delegates
|
||||||
|
dotnet_diagnostic.ca1848.severity = none
|
||||||
@@ -29,12 +29,12 @@ internal static class Mapper
|
|||||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||||
|
|
||||||
return languages
|
return languages
|
||||||
.Distinct()
|
.Map(lang => allCultures.Filter(ci => string.Equals(
|
||||||
.Map(
|
ci.ThreeLetterISOLanguageName,
|
||||||
lang => allCultures.Filter(
|
lang,
|
||||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
|
StringComparison.OrdinalIgnoreCase)))
|
||||||
.Sequence()
|
|
||||||
.Flatten()
|
.Flatten()
|
||||||
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<Artist
|
|||||||
async artist =>
|
async artist =>
|
||||||
{
|
{
|
||||||
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
|
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
|
||||||
List<string> languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes);
|
List<string> languageCodes = await _searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes);
|
||||||
return ProjectToViewModel(artist, languageCodes);
|
return ProjectToViewModel(artist, languageCodes);
|
||||||
},
|
},
|
||||||
() => Task.FromResult(Option<ArtistViewModel>.None));
|
() => Task.FromResult(Option<ArtistViewModel>.None));
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Net;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Artworks;
|
||||||
|
|
||||||
|
public record ArtworkContentTypeModel(string Path, string ContentType)
|
||||||
|
{
|
||||||
|
public static readonly ArtworkContentTypeModel None = new(string.Empty, string.Empty);
|
||||||
|
|
||||||
|
public bool IsExternalUrl => Artwork.IsExternalUrl(Path);
|
||||||
|
|
||||||
|
public bool HasContentType => !string.IsNullOrWhiteSpace(ContentType);
|
||||||
|
|
||||||
|
public string UrlWithContentType => string.IsNullOrWhiteSpace(ContentType)
|
||||||
|
? Path
|
||||||
|
: $"{Path}?contentType={WebUtility.UrlEncode(ContentType)}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Artworks;
|
||||||
|
|
||||||
|
public record GetArtwork(int Id) : IRequest<Either<BaseError, Artwork>>;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Artworks;
|
||||||
|
|
||||||
|
public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||||
|
: IRequestHandler<GetArtwork, Either<BaseError, Artwork>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory = dbContextFactory;
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Artwork>> Handle(
|
||||||
|
GetArtwork request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
Option<Artwork> artwork = await dbContext.Artwork
|
||||||
|
.AsNoTracking()
|
||||||
|
.SelectOneAsync(a => a.Id, a => a.Id == request.Id)
|
||||||
|
.MapT(Project);
|
||||||
|
|
||||||
|
return artwork.ToEither(BaseError.New("Artwork not found"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BaseError.New(ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Artwork Project(Artwork artwork) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = artwork.Id,
|
||||||
|
Path = artwork.Path,
|
||||||
|
ArtworkKind = artwork.ArtworkKind
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
using System.Net;
|
||||||
|
using ErsatzTV.Application.Artworks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
@@ -9,9 +11,12 @@ public record ChannelViewModel(
|
|||||||
string Group,
|
string Group,
|
||||||
string Categories,
|
string Categories,
|
||||||
int FFmpegProfileId,
|
int FFmpegProfileId,
|
||||||
string Logo,
|
ArtworkContentTypeModel Logo,
|
||||||
|
ChannelStreamSelectorMode StreamSelectorMode,
|
||||||
|
string StreamSelector,
|
||||||
string PreferredAudioLanguageCode,
|
string PreferredAudioLanguageCode,
|
||||||
string PreferredAudioTitle,
|
string PreferredAudioTitle,
|
||||||
|
ChannelProgressMode ProgressMode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId,
|
int? WatermarkId,
|
||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
@@ -19,4 +24,9 @@ public record ChannelViewModel(
|
|||||||
string PreferredSubtitleLanguageCode,
|
string PreferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode SubtitleMode,
|
ChannelSubtitleMode SubtitleMode,
|
||||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||||
string MusicVideoCreditsTemplate);
|
string MusicVideoCreditsTemplate,
|
||||||
|
ChannelSongVideoMode SongVideoMode,
|
||||||
|
ChannelActiveMode ActiveMode)
|
||||||
|
{
|
||||||
|
public string WebEncodedName => WebUtility.UrlEncode(Name);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Application.Artworks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
public record CreateChannel
|
public record CreateChannel(
|
||||||
(
|
|
||||||
string Name,
|
string Name,
|
||||||
string Number,
|
string Number,
|
||||||
string Group,
|
string Group,
|
||||||
string Categories,
|
string Categories,
|
||||||
int FFmpegProfileId,
|
int FFmpegProfileId,
|
||||||
string Logo,
|
ArtworkContentTypeModel Logo,
|
||||||
|
ChannelStreamSelectorMode StreamSelectorMode,
|
||||||
|
string StreamSelector,
|
||||||
string PreferredAudioLanguageCode,
|
string PreferredAudioLanguageCode,
|
||||||
string PreferredAudioTitle,
|
string PreferredAudioTitle,
|
||||||
|
ChannelProgressMode ProgressMode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId,
|
int? WatermarkId,
|
||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
string PreferredSubtitleLanguageCode,
|
string PreferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode SubtitleMode,
|
ChannelSubtitleMode SubtitleMode,
|
||||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||||
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, CreateChannelResult>>;
|
string MusicVideoCreditsTemplate,
|
||||||
|
ChannelSongVideoMode SongVideoMode,
|
||||||
|
ChannelActiveMode ActiveMode) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||||
|
|||||||
@@ -1,61 +1,69 @@
|
|||||||
using System.Globalization;
|
using System.Text.RegularExpressions;
|
||||||
using System.Text.RegularExpressions;
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Domain.Filler;
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Channel = ErsatzTV.Core.Domain.Channel;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
|
public class CreateChannelHandler(
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
ISearchTargets searchTargets)
|
||||||
|
: IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
|
||||||
|
|
||||||
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
|
|
||||||
|
|
||||||
public async Task<Either<BaseError, CreateChannelResult>> Handle(
|
public async Task<Either<BaseError, CreateChannelResult>> Handle(
|
||||||
CreateChannel request,
|
CreateChannel request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
||||||
return await validation.Apply(c => PersistChannel(dbContext, c));
|
return await validation.Apply(c => PersistChannel(dbContext, c));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
|
private async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
|
||||||
{
|
{
|
||||||
await dbContext.Channels.AddAsync(channel);
|
await dbContext.Channels.AddAsync(channel);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
searchTargets.SearchTargetsChanged();
|
||||||
|
await workerChannel.WriteAsync(new RefreshChannelList());
|
||||||
return new CreateChannelResult(channel.Id);
|
return new CreateChannelResult(channel.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, CreateChannel request) =>
|
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, CreateChannel request) =>
|
||||||
(ValidateName(request), await ValidateNumber(dbContext, request),
|
(ValidateName(request), await ValidateNumber(dbContext, request),
|
||||||
await FFmpegProfileMustExist(dbContext, request),
|
await FFmpegProfileMustExist(dbContext, request),
|
||||||
ValidatePreferredAudioLanguage(request),
|
|
||||||
ValidatePreferredSubtitleLanguage(request),
|
|
||||||
await WatermarkMustExist(dbContext, request),
|
await WatermarkMustExist(dbContext, request),
|
||||||
await FillerPresetMustExist(dbContext, request))
|
await FillerPresetMustExist(dbContext, request))
|
||||||
.Apply(
|
.Apply((
|
||||||
(
|
|
||||||
name,
|
name,
|
||||||
number,
|
number,
|
||||||
ffmpegProfileId,
|
ffmpegProfileId,
|
||||||
preferredAudioLanguageCode,
|
|
||||||
preferredSubtitleLanguageCode,
|
|
||||||
watermarkId,
|
watermarkId,
|
||||||
fillerPresetId) =>
|
fillerPresetId) =>
|
||||||
{
|
{
|
||||||
var artwork = new List<Artwork>();
|
var artwork = new List<Artwork>();
|
||||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
if (!string.IsNullOrWhiteSpace(request.Logo?.Path))
|
||||||
{
|
{
|
||||||
|
string logo = request.Logo.Path;
|
||||||
|
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
logo = logo.Replace("iptv/logos/", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
artwork.Add(
|
artwork.Add(
|
||||||
new Artwork
|
new Artwork
|
||||||
{
|
{
|
||||||
Path = request.Logo,
|
Path = logo,
|
||||||
ArtworkKind = ArtworkKind.Logo,
|
ArtworkKind = ArtworkKind.Logo,
|
||||||
|
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType)
|
||||||
|
? request.Logo.ContentType
|
||||||
|
: null,
|
||||||
DateAdded = DateTime.UtcNow,
|
DateAdded = DateTime.UtcNow,
|
||||||
DateUpdated = DateTime.UtcNow
|
DateUpdated = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
@@ -68,14 +76,19 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
|||||||
Group = request.Group,
|
Group = request.Group,
|
||||||
Categories = request.Categories,
|
Categories = request.Categories,
|
||||||
FFmpegProfileId = ffmpegProfileId,
|
FFmpegProfileId = ffmpegProfileId,
|
||||||
|
ProgressMode = request.ProgressMode,
|
||||||
StreamingMode = request.StreamingMode,
|
StreamingMode = request.StreamingMode,
|
||||||
Artwork = artwork,
|
Artwork = artwork,
|
||||||
PreferredAudioLanguageCode = preferredAudioLanguageCode,
|
StreamSelectorMode = request.StreamSelectorMode,
|
||||||
|
StreamSelector = request.StreamSelector,
|
||||||
|
PreferredAudioLanguageCode = request.PreferredAudioLanguageCode,
|
||||||
PreferredAudioTitle = request.PreferredAudioTitle,
|
PreferredAudioTitle = request.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = request.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = request.SubtitleMode,
|
SubtitleMode = request.SubtitleMode,
|
||||||
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
|
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
|
||||||
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate
|
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate,
|
||||||
|
SongVideoMode = request.SongVideoMode,
|
||||||
|
ActiveMode = request.ActiveMode
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (int id in watermarkId)
|
foreach (int id in watermarkId)
|
||||||
@@ -95,20 +108,6 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
|||||||
createChannel.NotEmpty(c => c.Name)
|
createChannel.NotEmpty(c => c.Name)
|
||||||
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
|
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
private static Validation<BaseError, string> ValidatePreferredAudioLanguage(CreateChannel createChannel) =>
|
|
||||||
Optional(createChannel.PreferredAudioLanguageCode ?? string.Empty)
|
|
||||||
.Filter(
|
|
||||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
|
||||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.ToValidation<BaseError>("Preferred audio language code is invalid");
|
|
||||||
|
|
||||||
private static Validation<BaseError, string> ValidatePreferredSubtitleLanguage(CreateChannel createChannel) =>
|
|
||||||
Optional(createChannel.PreferredSubtitleLanguageCode ?? string.Empty)
|
|
||||||
.Filter(
|
|
||||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
|
||||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.ToValidation<BaseError>("Preferred subtitle language code is invalid");
|
|
||||||
|
|
||||||
private static async Task<Validation<BaseError, string>> ValidateNumber(
|
private static async Task<Validation<BaseError, string>> ValidateNumber(
|
||||||
TvContext dbContext,
|
TvContext dbContext,
|
||||||
CreateChannel createChannel)
|
CreateChannel createChannel)
|
||||||
@@ -170,8 +169,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
|||||||
.Map(Optional)
|
.Map(Optional)
|
||||||
.Filter(c => c > 0)
|
.Filter(c => c > 0)
|
||||||
.MapT(_ => Optional(createChannel.FallbackFillerId))
|
.MapT(_ => Optional(createChannel.FallbackFillerId))
|
||||||
.Map(
|
.Map(o => o.ToValidation<BaseError>(
|
||||||
o => o.ToValidation<BaseError>(
|
|
||||||
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
|
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -12,30 +13,34 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
|
|||||||
{
|
{
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
private readonly ILocalFileSystem _localFileSystem;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
|
private readonly ISearchTargets _searchTargets;
|
||||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||||
|
|
||||||
public DeleteChannelHandler(
|
public DeleteChannelHandler(
|
||||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||||
IDbContextFactory<TvContext> dbContextFactory,
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
ILocalFileSystem localFileSystem)
|
ILocalFileSystem localFileSystem,
|
||||||
|
ISearchTargets searchTargets)
|
||||||
{
|
{
|
||||||
_workerChannel = workerChannel;
|
_workerChannel = workerChannel;
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_localFileSystem = localFileSystem;
|
_localFileSystem = localFileSystem;
|
||||||
|
_searchTargets = searchTargets;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Either<BaseError, Unit>> Handle(DeleteChannel request, CancellationToken cancellationToken)
|
public async Task<Either<BaseError, Unit>> Handle(DeleteChannel request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, Channel> validation = await ChannelMustExist(dbContext, request);
|
Validation<BaseError, Channel> validation = await ChannelMustExist(dbContext, request);
|
||||||
|
return await validation.Apply(c => DoDeletion(dbContext, c, cancellationToken));
|
||||||
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c, cancellationToken));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Unit> DoDeletion(TvContext dbContext, Channel channel, CancellationToken cancellationToken)
|
private async Task<Unit> DoDeletion(TvContext dbContext, Channel channel, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
dbContext.Channels.Remove(channel);
|
dbContext.Channels.Remove(channel);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_searchTargets.SearchTargetsChanged();
|
||||||
|
|
||||||
// delete channel data from channel guide cache
|
// delete channel data from channel guide cache
|
||||||
string cacheFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channel.Number}.xml");
|
string cacheFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channel.Number}.xml");
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,18 @@
|
|||||||
using System.Data;
|
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
|
using System.Net;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
using ErsatzTV.Core.Iptv;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.IO;
|
using Microsoft.IO;
|
||||||
|
using Scriban;
|
||||||
|
using Scriban.Runtime;
|
||||||
|
using WebMarkupMin.Core;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
@@ -14,16 +20,19 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
|
|||||||
{
|
{
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
private readonly ILocalFileSystem _localFileSystem;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
|
private readonly ILogger<RefreshChannelListHandler> _logger;
|
||||||
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
|
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
|
||||||
|
|
||||||
public RefreshChannelListHandler(
|
public RefreshChannelListHandler(
|
||||||
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
|
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
|
||||||
IDbContextFactory<TvContext> dbContextFactory,
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
ILocalFileSystem localFileSystem)
|
ILocalFileSystem localFileSystem,
|
||||||
|
ILogger<RefreshChannelListHandler> logger)
|
||||||
{
|
{
|
||||||
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
|
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_localFileSystem = localFileSystem;
|
_localFileSystem = localFileSystem;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(RefreshChannelList request, CancellationToken cancellationToken)
|
public async Task Handle(RefreshChannelList request, CancellationToken cancellationToken)
|
||||||
@@ -32,41 +41,67 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
|
|||||||
|
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
|
string templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "channel.sbntxt");
|
||||||
|
|
||||||
|
// fall back to default template
|
||||||
|
if (!_localFileSystem.FileExists(templateFileName))
|
||||||
|
{
|
||||||
|
templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "_channel.sbntxt");
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail if file doesn't exist
|
||||||
|
if (!_localFileSystem.FileExists(templateFileName))
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Unable to generate channel list without template file {File}; please restart ErsatzTV",
|
||||||
|
templateFileName);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var minifier = new XmlMinifier(
|
||||||
|
new XmlMinificationSettings
|
||||||
|
{
|
||||||
|
MinifyWhitespace = true,
|
||||||
|
RemoveXmlComments = true,
|
||||||
|
CollapseTagsWithoutContent = true
|
||||||
|
});
|
||||||
|
|
||||||
|
string text = await File.ReadAllTextAsync(templateFileName, cancellationToken);
|
||||||
|
var template = Template.Parse(text, templateFileName);
|
||||||
|
var templateContext = new XmlTemplateContext();
|
||||||
|
|
||||||
|
await using RecyclableMemoryStream ms = _recyclableMemoryStreamManager.GetStream();
|
||||||
await using var xml = XmlWriter.Create(
|
await using var xml = XmlWriter.Create(
|
||||||
ms,
|
ms,
|
||||||
new XmlWriterSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment });
|
new XmlWriterSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment });
|
||||||
|
|
||||||
await foreach (ChannelResult channel in GetChannels(dbContext).WithCancellation(cancellationToken))
|
await foreach (ChannelResult channel in GetChannels(dbContext).WithCancellation(cancellationToken))
|
||||||
{
|
{
|
||||||
await xml.WriteStartElementAsync(null, "channel", null);
|
bool hasLogo = !string.IsNullOrWhiteSpace(channel.ArtworkPath);
|
||||||
await xml.WriteAttributeStringAsync(null, "id", null, $"{channel.Number}.etv");
|
bool hasExternalLogo = hasLogo && Artwork.IsExternalUrl(channel.ArtworkPath);
|
||||||
|
|
||||||
await xml.WriteStartElementAsync(null, "display-name", null);
|
var data = new
|
||||||
await xml.WriteStringAsync($"{channel.Number} {channel.Name}");
|
|
||||||
await xml.WriteEndElementAsync(); // display-name (number and name)
|
|
||||||
|
|
||||||
await xml.WriteStartElementAsync(null, "display-name", null);
|
|
||||||
await xml.WriteStringAsync(channel.Number);
|
|
||||||
await xml.WriteEndElementAsync(); // display-name (number)
|
|
||||||
|
|
||||||
await xml.WriteStartElementAsync(null, "display-name", null);
|
|
||||||
await xml.WriteStringAsync(channel.Name);
|
|
||||||
await xml.WriteEndElementAsync(); // display-name (name)
|
|
||||||
|
|
||||||
foreach (string category in GetCategories(channel.Categories))
|
|
||||||
{
|
{
|
||||||
await xml.WriteStartElementAsync(null, "category", null);
|
ChannelId = ChannelIdentifier.FromNumber(channel.Number),
|
||||||
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
|
ChannelIdLegacy = ChannelIdentifier.LegacyFromNumber(channel.Number),
|
||||||
await xml.WriteStringAsync(category);
|
ChannelNumber = channel.Number,
|
||||||
await xml.WriteEndElementAsync(); // category
|
ChannelName = channel.Name,
|
||||||
}
|
ChannelCategories = GetCategories(channel.Categories),
|
||||||
|
ChannelHasExternalArtwork = hasExternalLogo,
|
||||||
|
ChannelHasArtwork = hasLogo,
|
||||||
|
ChannelArtworkPath = channel.ArtworkPath,
|
||||||
|
ChannelNameEncoded = WebUtility.UrlEncode(channel.Name)
|
||||||
|
};
|
||||||
|
|
||||||
await xml.WriteStartElementAsync(null, "icon", null);
|
var scriptObject = new ScriptObject();
|
||||||
await xml.WriteAttributeStringAsync(null, "src", null, GetIconUrl(channel));
|
scriptObject.Import(data);
|
||||||
await xml.WriteEndElementAsync(); // icon
|
templateContext.PushGlobal(scriptObject);
|
||||||
|
|
||||||
await xml.WriteEndElementAsync(); // channel
|
string result = await template.RenderAsync(templateContext);
|
||||||
|
|
||||||
|
MarkupMinificationResult minified = minifier.Minify(result);
|
||||||
|
await xml.WriteRawAsync(minified.MinifiedContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
await xml.FlushAsync();
|
await xml.FlushAsync();
|
||||||
@@ -83,8 +118,9 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
|
|||||||
const string QUERY = @"select C.Number, C.Name, C.Categories, A.Path as ArtworkPath
|
const string QUERY = @"select C.Number, C.Name, C.Categories, A.Path as ArtworkPath
|
||||||
from Channel C
|
from Channel C
|
||||||
left outer join Artwork A on C.Id = A.ChannelId and A.ArtworkKind = 2
|
left outer join Artwork A on C.Id = A.ChannelId and A.ArtworkKind = 2
|
||||||
where C.Id in (select ChannelId from Playout)
|
where C.Id in (select ChannelId from Playout) and C.ActiveMode = 0
|
||||||
order by CAST(C.Number as real)";
|
order by CAST(C.Number as double)";
|
||||||
|
// TODO: this needs to be fixed for sqlite/mariadb
|
||||||
|
|
||||||
await using var reader = (DbDataReader)await dbContext.Connection.ExecuteReaderAsync(QUERY);
|
await using var reader = (DbDataReader)await dbContext.Connection.ExecuteReaderAsync(QUERY);
|
||||||
Func<DbDataReader, ChannelResult> rowParser = reader.GetRowParser<ChannelResult>();
|
Func<DbDataReader, ChannelResult> rowParser = reader.GetRowParser<ChannelResult>();
|
||||||
@@ -106,11 +142,6 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
private static string GetIconUrl(ChannelResult channel) =>
|
|
||||||
string.IsNullOrWhiteSpace(channel.ArtworkPath)
|
|
||||||
? "{RequestBase}/iptv/images/ersatztv-500.png{AccessTokenUri}"
|
|
||||||
: $"{{RequestBase}}/iptv/logos/{channel.ArtworkPath}.jpg{{AccessTokenUri}}";
|
|
||||||
|
|
||||||
// ReSharper disable once ClassNeverInstantiated.Local
|
// ReSharper disable once ClassNeverInstantiated.Local
|
||||||
private record ChannelResult(string Number, string Name, string Categories, string ArtworkPath);
|
private sealed record ChannelResult(string Number, string Name, string Categories, string ArtworkPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Application.Artworks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
public record UpdateChannel
|
public record UpdateChannel(
|
||||||
(
|
|
||||||
int ChannelId,
|
int ChannelId,
|
||||||
string Name,
|
string Name,
|
||||||
string Number,
|
string Number,
|
||||||
string Group,
|
string Group,
|
||||||
string Categories,
|
string Categories,
|
||||||
int FFmpegProfileId,
|
int FFmpegProfileId,
|
||||||
string Logo,
|
ArtworkContentTypeModel Logo,
|
||||||
|
ChannelStreamSelectorMode StreamSelectorMode,
|
||||||
|
string StreamSelector,
|
||||||
string PreferredAudioLanguageCode,
|
string PreferredAudioLanguageCode,
|
||||||
string PreferredAudioTitle,
|
string PreferredAudioTitle,
|
||||||
|
ChannelProgressMode ProgressMode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId,
|
int? WatermarkId,
|
||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
string PreferredSubtitleLanguageCode,
|
string PreferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode SubtitleMode,
|
ChannelSubtitleMode SubtitleMode,
|
||||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||||
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, ChannelViewModel>>;
|
string MusicVideoCreditsTemplate,
|
||||||
|
ChannelSongVideoMode SongVideoMode,
|
||||||
|
ChannelActiveMode ActiveMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Globalization;
|
using System.Text.RegularExpressions;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Application.Subtitles;
|
using ErsatzTV.Application.Subtitles;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -12,26 +12,19 @@ using Channel = ErsatzTV.Core.Domain.Channel;
|
|||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
|
public class UpdateChannelHandler(
|
||||||
{
|
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
|
||||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
|
||||||
|
|
||||||
public UpdateChannelHandler(
|
|
||||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||||
IDbContextFactory<TvContext> dbContextFactory)
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
{
|
ISearchTargets searchTargets)
|
||||||
_workerChannel = workerChannel;
|
: IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
|
||||||
_dbContextFactory = dbContextFactory;
|
{
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Either<BaseError, ChannelViewModel>> Handle(
|
public async Task<Either<BaseError, ChannelViewModel>> Handle(
|
||||||
UpdateChannel request,
|
UpdateChannel request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
||||||
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
|
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
|
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
|
||||||
@@ -41,43 +34,72 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
|||||||
c.Group = update.Group;
|
c.Group = update.Group;
|
||||||
c.Categories = update.Categories;
|
c.Categories = update.Categories;
|
||||||
c.FFmpegProfileId = update.FFmpegProfileId;
|
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||||
|
c.StreamSelectorMode = update.StreamSelectorMode;
|
||||||
|
c.StreamSelector = update.StreamSelector;
|
||||||
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
|
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
|
||||||
c.PreferredAudioTitle = update.PreferredAudioTitle;
|
c.PreferredAudioTitle = update.PreferredAudioTitle;
|
||||||
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
|
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
|
||||||
c.SubtitleMode = update.SubtitleMode;
|
c.SubtitleMode = update.SubtitleMode;
|
||||||
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
||||||
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
|
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
|
||||||
c.Artwork ??= new List<Artwork>();
|
c.SongVideoMode = update.SongVideoMode;
|
||||||
|
c.ActiveMode = update.ActiveMode;
|
||||||
|
c.Artwork ??= [];
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
if (!string.IsNullOrWhiteSpace(update.Logo?.Path))
|
||||||
{
|
{
|
||||||
Option<Artwork> maybeLogo =
|
string logo = update.Logo.Path;
|
||||||
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
|
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
logo = logo.Replace("iptv/logos/", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
maybeLogo.Match(
|
Option<Artwork> maybeLogo = c.Artwork.Where(a => a.ArtworkKind == ArtworkKind.Logo).HeadOrNone();
|
||||||
artwork =>
|
foreach (Artwork artwork in maybeLogo)
|
||||||
{
|
{
|
||||||
artwork.Path = update.Logo;
|
artwork.Path = logo;
|
||||||
|
artwork.OriginalContentType = !string.IsNullOrEmpty(update.Logo.ContentType)
|
||||||
|
? update.Logo.ContentType
|
||||||
|
: null;
|
||||||
artwork.DateUpdated = DateTime.UtcNow;
|
artwork.DateUpdated = DateTime.UtcNow;
|
||||||
},
|
}
|
||||||
() =>
|
|
||||||
|
if (maybeLogo.IsNone)
|
||||||
{
|
{
|
||||||
var artwork = new Artwork
|
var artwork = new Artwork
|
||||||
{
|
{
|
||||||
Path = update.Logo,
|
Path = logo,
|
||||||
|
OriginalContentType = !string.IsNullOrEmpty(update.Logo.ContentType)
|
||||||
|
? update.Logo.ContentType
|
||||||
|
: null,
|
||||||
DateAdded = DateTime.UtcNow,
|
DateAdded = DateTime.UtcNow,
|
||||||
DateUpdated = DateTime.UtcNow,
|
DateUpdated = DateTime.UtcNow,
|
||||||
ArtworkKind = ArtworkKind.Logo
|
ArtworkKind = ArtworkKind.Logo
|
||||||
};
|
};
|
||||||
c.Artwork.Add(artwork);
|
c.Artwork.Add(artwork);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await dbContext.Entry(c)
|
||||||
|
.Collection(channel => channel.Artwork)
|
||||||
|
.LoadAsync();
|
||||||
|
|
||||||
|
foreach (Artwork artwork in c.Artwork.Where(x => x.ArtworkKind is ArtworkKind.Logo).ToList())
|
||||||
|
{
|
||||||
|
c.Artwork.Remove(artwork);
|
||||||
|
dbContext.Artwork.Remove(artwork);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.ProgressMode = update.ProgressMode;
|
||||||
c.StreamingMode = update.StreamingMode;
|
c.StreamingMode = update.StreamingMode;
|
||||||
c.WatermarkId = update.WatermarkId;
|
c.WatermarkId = update.WatermarkId;
|
||||||
c.FallbackFillerId = update.FallbackFillerId;
|
c.FallbackFillerId = update.FallbackFillerId;
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
searchTargets.SearchTargetsChanged();
|
||||||
|
|
||||||
if (c.SubtitleMode != ChannelSubtitleMode.None)
|
if (c.SubtitleMode != ChannelSubtitleMode.None)
|
||||||
{
|
{
|
||||||
Option<Playout> maybePlayout = await dbContext.Playouts
|
Option<Playout> maybePlayout = await dbContext.Playouts
|
||||||
@@ -85,18 +107,19 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
|||||||
|
|
||||||
foreach (Playout playout in maybePlayout)
|
foreach (Playout playout in maybePlayout)
|
||||||
{
|
{
|
||||||
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
|
await workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await workerChannel.WriteAsync(new RefreshChannelList());
|
||||||
|
|
||||||
return ProjectToViewModel(c);
|
return ProjectToViewModel(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
|
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
|
||||||
(await ChannelMustExist(dbContext, request), ValidateName(request),
|
(await ChannelMustExist(dbContext, request), ValidateName(request),
|
||||||
await ValidateNumber(dbContext, request),
|
await ValidateNumber(dbContext, request))
|
||||||
ValidatePreferredAudioLanguage(request))
|
.Apply((channelToUpdate, _, _) => channelToUpdate);
|
||||||
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
|
|
||||||
|
|
||||||
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
|
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
|
||||||
TvContext dbContext,
|
TvContext dbContext,
|
||||||
@@ -131,11 +154,4 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
|||||||
|
|
||||||
return BaseError.New("Channel number must be unique");
|
return BaseError.New("Channel number must be unique");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Validation<BaseError, string> ValidatePreferredAudioLanguage(UpdateChannel updateChannel) =>
|
|
||||||
Optional(updateChannel.PreferredAudioLanguageCode ?? string.Empty)
|
|
||||||
.Filter(
|
|
||||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
|
||||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.ToValidation<BaseError>("Preferred audio language code is invalid");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using ErsatzTV.Core.Api.Channels;
|
using ErsatzTV.Application.Artworks;
|
||||||
|
using ErsatzTV.Core.Api.Channels;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
@@ -14,8 +15,11 @@ internal static class Mapper
|
|||||||
channel.Categories,
|
channel.Categories,
|
||||||
channel.FFmpegProfileId,
|
channel.FFmpegProfileId,
|
||||||
GetLogo(channel),
|
GetLogo(channel),
|
||||||
|
channel.StreamSelectorMode,
|
||||||
|
channel.StreamSelector,
|
||||||
channel.PreferredAudioLanguageCode,
|
channel.PreferredAudioLanguageCode,
|
||||||
channel.PreferredAudioTitle,
|
channel.PreferredAudioTitle,
|
||||||
|
channel.ProgressMode,
|
||||||
channel.StreamingMode,
|
channel.StreamingMode,
|
||||||
channel.WatermarkId,
|
channel.WatermarkId,
|
||||||
channel.FallbackFillerId,
|
channel.FallbackFillerId,
|
||||||
@@ -23,7 +27,9 @@ internal static class Mapper
|
|||||||
channel.PreferredSubtitleLanguageCode,
|
channel.PreferredSubtitleLanguageCode,
|
||||||
channel.SubtitleMode,
|
channel.SubtitleMode,
|
||||||
channel.MusicVideoCreditsMode,
|
channel.MusicVideoCreditsMode,
|
||||||
channel.MusicVideoCreditsTemplate);
|
channel.MusicVideoCreditsTemplate,
|
||||||
|
channel.SongVideoMode,
|
||||||
|
channel.ActiveMode);
|
||||||
|
|
||||||
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
|
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
|
||||||
new(
|
new(
|
||||||
@@ -34,9 +40,27 @@ internal static class Mapper
|
|||||||
channel.PreferredAudioLanguageCode,
|
channel.PreferredAudioLanguageCode,
|
||||||
GetStreamingMode(channel));
|
GetStreamingMode(channel));
|
||||||
|
|
||||||
private static string GetLogo(Channel channel) =>
|
internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) =>
|
||||||
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
new(resolution.Height, resolution.Width);
|
||||||
.Match(a => a.Path, string.Empty);
|
|
||||||
|
internal static ResolutionAndBitrateViewModel ProjectToViewModel(Resolution resolution, int bitrate) =>
|
||||||
|
new(resolution.Height, resolution.Width, bitrate);
|
||||||
|
|
||||||
|
private static ArtworkContentTypeModel GetLogo(Channel channel)
|
||||||
|
{
|
||||||
|
Option<Artwork> maybeArtwork = channel.Artwork
|
||||||
|
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
|
||||||
|
.HeadOrNone();
|
||||||
|
|
||||||
|
foreach (Artwork artwork in maybeArtwork)
|
||||||
|
{
|
||||||
|
return artwork.IsExternalUrl()
|
||||||
|
? new ArtworkContentTypeModel(artwork.Path, string.Empty)
|
||||||
|
: new ArtworkContentTypeModel($"iptv/logos/{artwork.Path}", artwork.OriginalContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ArtworkContentTypeModel.None;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetStreamingMode(Channel channel) =>
|
private static string GetStreamingMode(Channel channel) =>
|
||||||
channel.StreamingMode switch
|
channel.StreamingMode switch
|
||||||
@@ -45,6 +69,7 @@ internal static class Mapper
|
|||||||
StreamingMode.TransportStreamHybrid => "MPEG-TS",
|
StreamingMode.TransportStreamHybrid => "MPEG-TS",
|
||||||
StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
|
StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
|
||||||
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
|
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
StreamingMode.HttpLiveStreamingSegmenterV2 => "HLS Segmenter V2",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(channel))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ using static ErsatzTV.Application.Channels.Mapper;
|
|||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>>
|
public class GetChannelByIdHandler(IChannelRepository channelRepository)
|
||||||
|
: IRequestHandler<GetChannelById, Option<ChannelViewModel>>
|
||||||
{
|
{
|
||||||
private readonly IChannelRepository _channelRepository;
|
|
||||||
|
|
||||||
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
|
||||||
|
|
||||||
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
|
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
|
||||||
_channelRepository.Get(request.Id)
|
channelRepository.GetChannel(request.Id)
|
||||||
.MapT(ProjectToViewModel);
|
.MapT(ProjectToViewModel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distinct.Any())
|
if (distinct.Count != 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
|
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ using ErsatzTV.Core.Iptv;
|
|||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
public record GetChannelGuide
|
public record GetChannelGuide(string Scheme, string Host, string BaseUrl, string AccessToken)
|
||||||
(string Scheme, string Host, string BaseUrl, string AccessToken) : IRequest<Either<BaseError, ChannelGuide>>;
|
: IRequest<Either<BaseError, ChannelGuide>>;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Iptv;
|
using ErsatzTV.Core.Iptv;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
@@ -29,6 +31,12 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
var inactiveChannelNumbers = dbContext.Channels
|
||||||
|
.Where(c => c.ActiveMode != ChannelActiveMode.Active)
|
||||||
|
.Select(c => c.Number)
|
||||||
|
.AsEnumerable()
|
||||||
|
.Select(n => $"{n}.xml")
|
||||||
|
.ToImmutableHashSet();
|
||||||
|
|
||||||
string channelsFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
|
string channelsFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
|
||||||
if (!_localFileSystem.FileExists(channelsFile))
|
if (!_localFileSystem.FileExists(channelsFile))
|
||||||
@@ -36,10 +44,12 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
|
|||||||
return BaseError.New($"Required file {channelsFile} is missing");
|
return BaseError.New($"Required file {channelsFile} is missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
string accessTokenUri = string.Empty;
|
long mtime = File.GetLastWriteTime(channelsFile).Ticks;
|
||||||
|
|
||||||
|
var accessTokenUri = $"?v={mtime}";
|
||||||
if (!string.IsNullOrWhiteSpace(request.AccessToken))
|
if (!string.IsNullOrWhiteSpace(request.AccessToken))
|
||||||
{
|
{
|
||||||
accessTokenUri = $"?access_token={request.AccessToken}";
|
accessTokenUri += $"&access_token={request.AccessToken}";
|
||||||
}
|
}
|
||||||
|
|
||||||
string channelsFragment = await File.ReadAllTextAsync(channelsFile, Encoding.UTF8, cancellationToken);
|
string channelsFragment = await File.ReadAllTextAsync(channelsFile, Encoding.UTF8, cancellationToken);
|
||||||
@@ -58,6 +68,11 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inactiveChannelNumbers.Contains(Path.GetFileName(fileName)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
string channelDataFragment = await File.ReadAllTextAsync(fileName, Encoding.UTF8, cancellationToken);
|
string channelDataFragment = await File.ReadAllTextAsync(fileName, Encoding.UTF8, cancellationToken);
|
||||||
|
|
||||||
channelDataFragment = channelDataFragment
|
channelDataFragment = channelDataFragment
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using ErsatzTV.Core.Hdhr;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Hdhr;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
@@ -11,5 +12,6 @@ public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<Li
|
|||||||
|
|
||||||
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
|
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
|
||||||
_channelRepository.GetAll()
|
_channelRepository.GetAll()
|
||||||
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
|
.Map(channels => channels.Where(c => c.ActiveMode is ChannelActiveMode.Active)
|
||||||
|
.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,10 @@ using ErsatzTV.Core.Iptv;
|
|||||||
|
|
||||||
namespace ErsatzTV.Application.Channels;
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
public record GetChannelPlaylist
|
public record GetChannelPlaylist(
|
||||||
(string Scheme, string Host, string BaseUrl, string Mode, string AccessToken) : IRequest<ChannelPlaylist>;
|
string Scheme,
|
||||||
|
string Host,
|
||||||
|
string BaseUrl,
|
||||||
|
string Mode,
|
||||||
|
string UserAgent,
|
||||||
|
string AccessToken) : IRequest<ChannelPlaylist>;
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
|
|||||||
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
|
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
|
||||||
_channelRepository.GetAll()
|
_channelRepository.GetAll()
|
||||||
.Map(channels => EnsureMode(channels, request.Mode))
|
.Map(channels => EnsureMode(channels, request.Mode))
|
||||||
.Map(
|
.Map(channels => new ChannelPlaylist(
|
||||||
channels => new ChannelPlaylist(
|
|
||||||
request.Scheme,
|
request.Scheme,
|
||||||
request.Host,
|
request.Host,
|
||||||
request.BaseUrl,
|
request.BaseUrl,
|
||||||
channels,
|
channels,
|
||||||
|
request.UserAgent,
|
||||||
request.AccessToken));
|
request.AccessToken));
|
||||||
|
|
||||||
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
|
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
|
||||||
@@ -27,12 +27,21 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
|
|||||||
var result = new List<Channel>();
|
var result = new List<Channel>();
|
||||||
foreach (Channel channel in channels)
|
foreach (Channel channel in channels)
|
||||||
{
|
{
|
||||||
|
if (channel.ActiveMode is not ChannelActiveMode.Active)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
switch (mode.ToLowerInvariant())
|
switch (mode.ToLowerInvariant())
|
||||||
{
|
{
|
||||||
case "segmenter":
|
case "segmenter":
|
||||||
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
|
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
|
||||||
result.Add(channel);
|
result.Add(channel);
|
||||||
break;
|
break;
|
||||||
|
case "segmenter-v2":
|
||||||
|
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenterV2;
|
||||||
|
result.Add(channel);
|
||||||
|
break;
|
||||||
case "hls-direct":
|
case "hls-direct":
|
||||||
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
|
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
|
||||||
result.Add(channel);
|
result.Add(channel);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
|
public record GetChannelResolutionAndBitrate(string ChannelNumber) : IRequest<Option<ResolutionAndBitrateViewModel>>;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
|
public class GetChannelResolutionAndBitrateHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||||
|
: IRequestHandler<GetChannelResolutionAndBitrate, Option<ResolutionAndBitrateViewModel>>
|
||||||
|
{
|
||||||
|
public async Task<Option<ResolutionAndBitrateViewModel>> Handle(
|
||||||
|
GetChannelResolutionAndBitrate request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
Option<Channel> maybeChannel = await dbContext.Channels
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(c => c.FFmpegProfile)
|
||||||
|
.ThenInclude(ff => ff.Resolution)
|
||||||
|
.SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber);
|
||||||
|
|
||||||
|
return maybeChannel.Map(c => Mapper.ProjectToViewModel(
|
||||||
|
c.FFmpegProfile.Resolution,
|
||||||
|
(int)((c.FFmpegProfile.VideoBitrate * 1000 + c.FFmpegProfile.AudioBitrate * 1000) * 1.2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
|
public record GetChannelStreamSelectors : IRequest<List<string>>;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
|
public class GetChannelStreamSelectorsHandler(ILocalFileSystem localFileSystem)
|
||||||
|
: IRequestHandler<GetChannelStreamSelectors, List<string>>
|
||||||
|
{
|
||||||
|
public Task<List<string>> Handle(GetChannelStreamSelectors request, CancellationToken cancellationToken) =>
|
||||||
|
localFileSystem.ListFiles(FileSystemLayout.ChannelStreamSelectorsFolder)
|
||||||
|
.Map(Path.GetFileName)
|
||||||
|
.ToList()
|
||||||
|
.AsTask();
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
|
public record ResolutionAndBitrateViewModel(int Height, int Width, int Bitrate);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
|
public record ResolutionViewModel(int Height, int Width);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Scriban;
|
||||||
|
using Scriban.Parsing;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels;
|
||||||
|
|
||||||
|
public class XmlTemplateContext : TemplateContext
|
||||||
|
{
|
||||||
|
public override TemplateContext Write(SourceSpan span, object textAsObject)
|
||||||
|
=> base.Write(span, textAsObject is string text ? WebUtility.HtmlEncode(text) : textAsObject);
|
||||||
|
|
||||||
|
public override ValueTask<TemplateContext> WriteAsync(SourceSpan span, object textAsObject)
|
||||||
|
=> base.WriteAsync(span, textAsObject is string text ? WebUtility.HtmlEncode(text) : textAsObject);
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
using ErsatzTV.Core;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Configuration;
|
|
||||||
|
|
||||||
public record UpdateGeneralSettings(GeneralSettingsViewModel GeneralSettings) : IRequest<Either<BaseError, Unit>>;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using ErsatzTV.Core;
|
|
||||||
using ErsatzTV.Core.Domain;
|
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
|
||||||
using Serilog.Core;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Configuration;
|
|
||||||
|
|
||||||
public class UpdateGeneralSettingsHandler : IRequestHandler<UpdateGeneralSettings, Either<BaseError, Unit>>
|
|
||||||
{
|
|
||||||
private readonly IConfigElementRepository _configElementRepository;
|
|
||||||
private readonly LoggingLevelSwitch _loggingLevelSwitch;
|
|
||||||
|
|
||||||
public UpdateGeneralSettingsHandler(
|
|
||||||
LoggingLevelSwitch loggingLevelSwitch,
|
|
||||||
IConfigElementRepository configElementRepository)
|
|
||||||
{
|
|
||||||
_loggingLevelSwitch = loggingLevelSwitch;
|
|
||||||
_configElementRepository = configElementRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Either<BaseError, Unit>> Handle(
|
|
||||||
UpdateGeneralSettings request,
|
|
||||||
CancellationToken cancellationToken) => await ApplyUpdate(request.GeneralSettings);
|
|
||||||
|
|
||||||
private async Task<Unit> ApplyUpdate(GeneralSettingsViewModel generalSettings)
|
|
||||||
{
|
|
||||||
await _configElementRepository.Upsert(ConfigElementKey.MinimumLogLevel, generalSettings.MinimumLogLevel);
|
|
||||||
_loggingLevelSwitch.MinimumLevel = generalSettings.MinimumLogLevel;
|
|
||||||
|
|
||||||
return Unit.Default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,7 @@ public class UpdateLibraryRefreshIntervalHandler :
|
|||||||
UpdateLibraryRefreshInterval request,
|
UpdateLibraryRefreshInterval request,
|
||||||
CancellationToken cancellationToken) =>
|
CancellationToken cancellationToken) =>
|
||||||
Validate(request)
|
Validate(request)
|
||||||
.MapT(
|
.MapT(_ => _configElementRepository.Upsert(
|
||||||
_ => _configElementRepository.Upsert(
|
|
||||||
ConfigElementKey.LibraryRefreshInterval,
|
ConfigElementKey.LibraryRefreshInterval,
|
||||||
request.LibraryRefreshInterval))
|
request.LibraryRefreshInterval))
|
||||||
.Bind(v => v.ToEitherAsync());
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public record UpdateLoggingSettings(LoggingSettingsViewModel LoggingSettings) : IRequest<Either<BaseError, Unit>>;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public class UpdateLoggingSettingsHandler : IRequestHandler<UpdateLoggingSettings, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
private readonly LoggingLevelSwitches _loggingLevelSwitches;
|
||||||
|
|
||||||
|
public UpdateLoggingSettingsHandler(
|
||||||
|
LoggingLevelSwitches loggingLevelSwitches,
|
||||||
|
IConfigElementRepository configElementRepository)
|
||||||
|
{
|
||||||
|
_loggingLevelSwitches = loggingLevelSwitches;
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
UpdateLoggingSettings request,
|
||||||
|
CancellationToken cancellationToken) => await ApplyUpdate(request.LoggingSettings);
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdate(LoggingSettingsViewModel loggingSettings)
|
||||||
|
{
|
||||||
|
await _configElementRepository.Upsert(ConfigElementKey.MinimumLogLevel, loggingSettings.DefaultMinimumLogLevel);
|
||||||
|
_loggingLevelSwitches.DefaultLevelSwitch.MinimumLevel = loggingSettings.DefaultMinimumLogLevel;
|
||||||
|
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.MinimumLogLevelScanning,
|
||||||
|
loggingSettings.ScanningMinimumLogLevel);
|
||||||
|
_loggingLevelSwitches.ScanningLevelSwitch.MinimumLevel = loggingSettings.ScanningMinimumLogLevel;
|
||||||
|
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.MinimumLogLevelScheduling,
|
||||||
|
loggingSettings.SchedulingMinimumLogLevel);
|
||||||
|
_loggingLevelSwitches.SchedulingLevelSwitch.MinimumLevel = loggingSettings.SchedulingMinimumLogLevel;
|
||||||
|
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.MinimumLogLevelStreaming,
|
||||||
|
loggingSettings.StreamingMinimumLogLevel);
|
||||||
|
_loggingLevelSwitches.StreamingLevelSwitch.MinimumLevel = loggingSettings.StreamingMinimumLogLevel;
|
||||||
|
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.MinimumLogLevelHttp,
|
||||||
|
loggingSettings.HttpMinimumLogLevel);
|
||||||
|
_loggingLevelSwitches.HttpLevelSwitch.MinimumLevel = loggingSettings.HttpMinimumLogLevel;
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Channels;
|
using System.Globalization;
|
||||||
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Application.Playouts;
|
using ErsatzTV.Application.Playouts;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -45,7 +46,8 @@ public class UpdatePlayoutSettingsHandler : IRequestHandler<UpdatePlayoutSetting
|
|||||||
List<Playout> playouts = await dbContext.Playouts
|
List<Playout> playouts = await dbContext.Playouts
|
||||||
.Include(p => p.Channel)
|
.Include(p => p.Channel)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
|
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number, CultureInfo.InvariantCulture))
|
||||||
|
.Map(p => p.Id))
|
||||||
{
|
{
|
||||||
await _workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Continue));
|
await _workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Continue));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public record UpdateXmltvSettings(XmltvSettingsViewModel XmltvSettings) : IRequest<Either<BaseError, Unit>>;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Threading.Channels;
|
||||||
|
using ErsatzTV.Application.Channels;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public class UpdateXmltvSettingsHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> workerChannel)
|
||||||
|
: IRequestHandler<UpdateXmltvSettings, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
UpdateXmltvSettings request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int playoutDaysToBuild =
|
||||||
|
await configElementRepository
|
||||||
|
.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
|
||||||
|
.IfNoneAsync(2);
|
||||||
|
|
||||||
|
if (playoutDaysToBuild < request.XmltvSettings.DaysToBuild)
|
||||||
|
{
|
||||||
|
return BaseError.New(
|
||||||
|
$"XMLTV days to build ({request.XmltvSettings.DaysToBuild}) cannot be greater than Playout days to build ({playoutDaysToBuild})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ApplyUpdate(request.XmltvSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdate(XmltvSettingsViewModel xmltvSettings)
|
||||||
|
{
|
||||||
|
await configElementRepository.Upsert(ConfigElementKey.XmltvTimeZone, xmltvSettings.TimeZone);
|
||||||
|
await configElementRepository.Upsert(ConfigElementKey.XmltvDaysToBuild, xmltvSettings.DaysToBuild);
|
||||||
|
|
||||||
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
foreach (string channelNumber in await dbContext.Channels.Map(c => c.Number).ToListAsync())
|
||||||
|
{
|
||||||
|
await workerChannel.WriteAsync(new RefreshChannelData(channelNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using Serilog.Events;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Configuration;
|
|
||||||
|
|
||||||
public class GeneralSettingsViewModel
|
|
||||||
{
|
|
||||||
public LogEventLevel MinimumLogLevel { get; set; }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public class LoggingSettingsViewModel
|
||||||
|
{
|
||||||
|
public LogEventLevel DefaultMinimumLogLevel { get; set; }
|
||||||
|
public LogEventLevel ScanningMinimumLogLevel { get; set; }
|
||||||
|
public LogEventLevel SchedulingMinimumLogLevel { get; set; }
|
||||||
|
public LogEventLevel StreamingMinimumLogLevel { get; set; }
|
||||||
|
public LogEventLevel HttpMinimumLogLevel { get; set; }
|
||||||
|
}
|
||||||
@@ -13,5 +13,5 @@ public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKe
|
|||||||
public Task<Option<ConfigElementViewModel>> Handle(
|
public Task<Option<ConfigElementViewModel>> Handle(
|
||||||
GetConfigElementByKey request,
|
GetConfigElementByKey request,
|
||||||
CancellationToken cancellationToken) =>
|
CancellationToken cancellationToken) =>
|
||||||
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
|
_configElementRepository.GetConfigElement(request.Key).MapT(ProjectToViewModel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace ErsatzTV.Application.Configuration;
|
|
||||||
|
|
||||||
public record GetGeneralSettings : IRequest<GeneralSettingsViewModel>;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
|
||||||
using Serilog.Events;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Configuration;
|
|
||||||
|
|
||||||
public class GetGeneralSettingsHandler : IRequestHandler<GetGeneralSettings, GeneralSettingsViewModel>
|
|
||||||
{
|
|
||||||
private readonly IConfigElementRepository _configElementRepository;
|
|
||||||
|
|
||||||
public GetGeneralSettingsHandler(IConfigElementRepository configElementRepository) =>
|
|
||||||
_configElementRepository = configElementRepository;
|
|
||||||
|
|
||||||
public async Task<GeneralSettingsViewModel> Handle(GetGeneralSettings request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Option<LogEventLevel> maybeLogLevel =
|
|
||||||
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevel);
|
|
||||||
|
|
||||||
return new GeneralSettingsViewModel
|
|
||||||
{
|
|
||||||
MinimumLogLevel = await maybeLogLevel.IfNoneAsync(LogEventLevel.Information)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public record GetLoggingSettings : IRequest<LoggingSettingsViewModel>;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public class GetLoggingSettingsHandler : IRequestHandler<GetLoggingSettings, LoggingSettingsViewModel>
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
|
||||||
|
public GetLoggingSettingsHandler(IConfigElementRepository configElementRepository) =>
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
|
||||||
|
public async Task<LoggingSettingsViewModel> Handle(GetLoggingSettings request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Option<LogEventLevel> maybeDefaultLevel =
|
||||||
|
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevel);
|
||||||
|
|
||||||
|
Option<LogEventLevel> maybeScanningLevel =
|
||||||
|
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelScanning);
|
||||||
|
|
||||||
|
Option<LogEventLevel> maybeSchedulingLevel =
|
||||||
|
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelScheduling);
|
||||||
|
|
||||||
|
Option<LogEventLevel> maybeStreamingLevel =
|
||||||
|
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelStreaming);
|
||||||
|
|
||||||
|
Option<LogEventLevel> maybeHttpLevel =
|
||||||
|
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelHttp);
|
||||||
|
|
||||||
|
return new LoggingSettingsViewModel
|
||||||
|
{
|
||||||
|
DefaultMinimumLogLevel = await maybeDefaultLevel.IfNoneAsync(LogEventLevel.Information),
|
||||||
|
ScanningMinimumLogLevel = await maybeScanningLevel.IfNoneAsync(LogEventLevel.Information),
|
||||||
|
SchedulingMinimumLogLevel = await maybeSchedulingLevel.IfNoneAsync(LogEventLevel.Information),
|
||||||
|
StreamingMinimumLogLevel = await maybeStreamingLevel.IfNoneAsync(LogEventLevel.Information),
|
||||||
|
HttpMinimumLogLevel = await maybeHttpLevel.IfNoneAsync(LogEventLevel.Information)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public record GetXmltvSettings : IRequest<XmltvSettingsViewModel>;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public class GetXmltvSettingsHandler(IConfigElementRepository configElementRepository)
|
||||||
|
: IRequestHandler<GetXmltvSettings, XmltvSettingsViewModel>
|
||||||
|
{
|
||||||
|
public async Task<XmltvSettingsViewModel> Handle(GetXmltvSettings request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Option<int> daysToBuild = await configElementRepository.GetValue<int>(ConfigElementKey.XmltvDaysToBuild);
|
||||||
|
|
||||||
|
Option<XmltvTimeZone> maybeTimeZone =
|
||||||
|
await configElementRepository.GetValue<XmltvTimeZone>(ConfigElementKey.XmltvTimeZone);
|
||||||
|
|
||||||
|
return new XmltvSettingsViewModel
|
||||||
|
{
|
||||||
|
DaysToBuild = await daysToBuild.IfNoneAsync(2),
|
||||||
|
TimeZone = await maybeTimeZone.IfNoneAsync(XmltvTimeZone.Local)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public class XmltvSettingsViewModel
|
||||||
|
{
|
||||||
|
public int DaysToBuild { get; set; }
|
||||||
|
public XmltvTimeZone TimeZone { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ErsatzTV.Application.Configuration;
|
||||||
|
|
||||||
|
public enum XmltvTimeZone
|
||||||
|
{
|
||||||
|
Local = 0,
|
||||||
|
Utc = 1
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Application.Libraries;
|
using ErsatzTV.Application.Libraries;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
@@ -69,7 +70,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
|
|||||||
{
|
{
|
||||||
var arguments = new List<string>
|
var arguments = new List<string>
|
||||||
{
|
{
|
||||||
"scan-emby-collections", request.EmbyMediaSourceId.ToString()
|
"scan-emby-collections", request.EmbyMediaSourceId.ToString(CultureInfo.InvariantCulture)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (request.ForceScan)
|
if (request.ForceScan)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Channels;
|
using System.Globalization;
|
||||||
|
using System.Threading.Channels;
|
||||||
using ErsatzTV.Application.Libraries;
|
using ErsatzTV.Application.Libraries;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Errors;
|
using ErsatzTV.Core.Errors;
|
||||||
@@ -58,7 +59,7 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
|
|||||||
{
|
{
|
||||||
var arguments = new List<string>
|
var arguments = new List<string>
|
||||||
{
|
{
|
||||||
"scan-emby", request.EmbyLibraryId.ToString()
|
"scan-emby", request.EmbyLibraryId.ToString(CultureInfo.InvariantCulture)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (request.ForceScan)
|
if (request.ForceScan)
|
||||||
|
|||||||
@@ -52,5 +52,5 @@ public class SaveEmbySecretsHandler : IRequestHandler<SaveEmbySecrets, Either<Ba
|
|||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
|
private sealed record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public class SynchronizeEmbyLibrariesHandler : IRequestHandler<SynchronizeEmbyLi
|
|||||||
toAdd,
|
toAdd,
|
||||||
toRemove,
|
toRemove,
|
||||||
toUpdate);
|
toUpdate);
|
||||||
if (ids.Any())
|
if (ids.Count != 0)
|
||||||
{
|
{
|
||||||
await _searchIndex.RemoveItems(ids);
|
await _searchIndex.RemoveItems(ids);
|
||||||
_searchIndex.Commit();
|
_searchIndex.Commit();
|
||||||
@@ -102,9 +102,7 @@ public class SynchronizeEmbyLibrariesHandler : IRequestHandler<SynchronizeEmbyLi
|
|||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ConnectionParameters(
|
private sealed record ConnectionParameters(EmbyMediaSource EmbyMediaSource, EmbyConnection ActiveConnection)
|
||||||
EmbyMediaSource EmbyMediaSource,
|
|
||||||
EmbyConnection ActiveConnection)
|
|
||||||
{
|
{
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmb
|
|||||||
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
|
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
|
||||||
foreach (EmbyMediaSource mediaSource in mediaSources)
|
foreach (EmbyMediaSource mediaSource in mediaSources)
|
||||||
{
|
{
|
||||||
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
|
|
||||||
await _scannerWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
|
await _scannerWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace ErsatzTV.Application.Emby;
|
namespace ErsatzTV.Application.Emby;
|
||||||
|
|
||||||
public record UpdateEmbyLibraryPreferences
|
public record UpdateEmbyLibraryPreferences(List<EmbyLibraryPreference> Preferences) : IRequest<Either<BaseError, Unit>>;
|
||||||
(List<EmbyLibraryPreference> Preferences) : IRequest<Either<BaseError, Unit>>;
|
|
||||||
|
|
||||||
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
|
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ public class UpdateEmbyPathReplacementsHandler : IRequestHandler<UpdateEmbyPathR
|
|||||||
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
|
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
|
||||||
UpdateEmbyPathReplacements request) =>
|
UpdateEmbyPathReplacements request) =>
|
||||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
|
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
|
||||||
.Map(
|
.Map(v => v.ToValidation<BaseError>(
|
||||||
v => v.ToValidation<BaseError>(
|
|
||||||
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
|
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ public record EmbyLibraryViewModel(
|
|||||||
LibraryMediaKind MediaKind,
|
LibraryMediaKind MediaKind,
|
||||||
bool ShouldSyncItems,
|
bool ShouldSyncItems,
|
||||||
int MediaSourceId)
|
int MediaSourceId)
|
||||||
: LibraryViewModel("Emby", Id, Name, MediaKind, MediaSourceId);
|
: LibraryViewModel("Emby", Id, Name, MediaKind, MediaSourceId, string.Empty);
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
|
|||||||
|
|
||||||
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
|
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
|
||||||
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
|
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
|
||||||
.Map(
|
.Map(v => v.ToValidation<BaseError>(
|
||||||
v => v.ToValidation<BaseError>(
|
|
||||||
"Emby media source does not exist."));
|
"Emby media source does not exist."));
|
||||||
|
|
||||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
||||||
@@ -76,7 +75,7 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
|
|||||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ConnectionParameters(
|
private sealed record ConnectionParameters(
|
||||||
EmbyMediaSource EmbyMediaSource,
|
EmbyMediaSource EmbyMediaSource,
|
||||||
EmbyConnection ActiveConnection)
|
EmbyConnection ActiveConnection)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
namespace ErsatzTV.Application.Emby;
|
namespace ErsatzTV.Application.Emby;
|
||||||
|
|
||||||
public record GetEmbyPathReplacementsBySourceId
|
public record GetEmbyPathReplacementsBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
|
||||||
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
|
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<NoWarn>VSTHRD200</NoWarn>
|
<NoWarn>VSTHRD200</NoWarn>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AnalysisLevel>latest-Recommended</AnalysisLevel>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Bugsnag" Version="3.1.0" />
|
<PackageReference Include="Bugsnag" Version="4.0.0" />
|
||||||
<PackageReference Include="CliWrap" Version="3.6.3" />
|
<PackageReference Include="CliWrap" Version="3.9.0" />
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
<PackageReference Include="MediatR" Version="[12.5.0]" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.6.40">
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="2.0.0" />
|
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
|
||||||
<PackageReference Include="Winista.MimeDetect" Version="1.0.1" />
|
<PackageReference Include="WebMarkupMin.Core" Version="2.19.0" />
|
||||||
|
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artists_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artists_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artworks_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
@@ -33,7 +34,10 @@
|
|||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plex_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plex_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=scheduling_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=scheduling_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
@@ -41,5 +45,7 @@
|
|||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=subtitles_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=subtitles_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=television_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=television_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=templates_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=templates_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=troubleshooting_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=validators/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Cqueries/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Cqueries/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||||
@@ -2,5 +2,4 @@
|
|||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||||
|
|
||||||
public record CopyFFmpegProfile
|
public record CopyFFmpegProfile(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||||
(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||||
@@ -8,9 +10,13 @@ public class
|
|||||||
CopyFFmpegProfileHandler : IRequestHandler<CopyFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
|
CopyFFmpegProfileHandler : IRequestHandler<CopyFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
|
||||||
{
|
{
|
||||||
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||||
|
private readonly ISearchTargets _searchTargets;
|
||||||
|
|
||||||
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
|
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository, ISearchTargets searchTargets)
|
||||||
|
{
|
||||||
_ffmpegProfileRepository = ffmpegProfileRepository;
|
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||||
|
_searchTargets = searchTargets;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
|
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
|
||||||
CopyFFmpegProfile request,
|
CopyFFmpegProfile request,
|
||||||
@@ -19,14 +25,17 @@ public class
|
|||||||
.MapT(PerformCopy)
|
.MapT(PerformCopy)
|
||||||
.Bind(v => v.ToEitherAsync());
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
private Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request) =>
|
private async Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request)
|
||||||
_ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name)
|
{
|
||||||
.Map(ProjectToViewModel);
|
FFmpegProfile copy = await _ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name);
|
||||||
|
_searchTargets.SearchTargetsChanged();
|
||||||
|
return ProjectToViewModel(copy);
|
||||||
|
}
|
||||||
|
|
||||||
private Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
|
private static Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
|
||||||
ValidateName(request).AsTask().MapT(_ => request);
|
ValidateName(request).AsTask().MapT(_ => request);
|
||||||
|
|
||||||
private Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
|
private static Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
|
||||||
request.NotEmpty(x => x.Name)
|
request.NotEmpty(x => x.Name)
|
||||||
.Bind(_ => request.NotLongerThan(50)(x => x.Name));
|
.Bind(_ => request.NotLongerThan(50)(x => x.Name));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,24 @@ public record CreateFFmpegProfile(
|
|||||||
string Name,
|
string Name,
|
||||||
int ThreadCount,
|
int ThreadCount,
|
||||||
HardwareAccelerationKind HardwareAcceleration,
|
HardwareAccelerationKind HardwareAcceleration,
|
||||||
|
string VaapiDisplay,
|
||||||
VaapiDriver VaapiDriver,
|
VaapiDriver VaapiDriver,
|
||||||
string VaapiDevice,
|
string VaapiDevice,
|
||||||
int? QsvExtraHardwareFrames,
|
int? QsvExtraHardwareFrames,
|
||||||
int ResolutionId,
|
int ResolutionId,
|
||||||
|
ScalingBehavior ScalingBehavior,
|
||||||
FFmpegProfileVideoFormat VideoFormat,
|
FFmpegProfileVideoFormat VideoFormat,
|
||||||
|
string VideoProfile,
|
||||||
|
string VideoPreset,
|
||||||
|
bool AllowBFrames,
|
||||||
FFmpegProfileBitDepth BitDepth,
|
FFmpegProfileBitDepth BitDepth,
|
||||||
int VideoBitrate,
|
int VideoBitrate,
|
||||||
int VideoBufferSize,
|
int VideoBufferSize,
|
||||||
|
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
|
||||||
FFmpegProfileAudioFormat AudioFormat,
|
FFmpegProfileAudioFormat AudioFormat,
|
||||||
int AudioBitrate,
|
int AudioBitrate,
|
||||||
int AudioBufferSize,
|
int AudioBufferSize,
|
||||||
bool NormalizeLoudness,
|
NormalizeLoudnessMode NormalizeLoudnessMode,
|
||||||
int AudioChannels,
|
int AudioChannels,
|
||||||
int AudioSampleRate,
|
int AudioSampleRate,
|
||||||
bool NormalizeFramerate,
|
bool NormalizeFramerate,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -10,9 +11,13 @@ public class CreateFFmpegProfileHandler :
|
|||||||
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
|
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly ISearchTargets _searchTargets;
|
||||||
|
|
||||||
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||||
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_searchTargets = searchTargets;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
|
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
|
||||||
CreateFFmpegProfile request,
|
CreateFFmpegProfile request,
|
||||||
@@ -23,21 +28,21 @@ public class CreateFFmpegProfileHandler :
|
|||||||
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
|
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
|
private async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
|
||||||
TvContext dbContext,
|
TvContext dbContext,
|
||||||
FFmpegProfile ffmpegProfile)
|
FFmpegProfile ffmpegProfile)
|
||||||
{
|
{
|
||||||
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
|
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
_searchTargets.SearchTargetsChanged();
|
||||||
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
|
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Validation<BaseError, FFmpegProfile>> Validate(
|
private static async Task<Validation<BaseError, FFmpegProfile>> Validate(
|
||||||
TvContext dbContext,
|
TvContext dbContext,
|
||||||
CreateFFmpegProfile request) =>
|
CreateFFmpegProfile request) =>
|
||||||
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
|
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
|
||||||
.Apply(
|
.Apply((name, threadCount, resolutionId) => new FFmpegProfile
|
||||||
(name, threadCount, resolutionId) => new FFmpegProfile
|
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
ThreadCount = threadCount,
|
ThreadCount = threadCount,
|
||||||
@@ -46,14 +51,19 @@ public class CreateFFmpegProfileHandler :
|
|||||||
VaapiDevice = request.VaapiDevice,
|
VaapiDevice = request.VaapiDevice,
|
||||||
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
|
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
|
||||||
ResolutionId = resolutionId,
|
ResolutionId = resolutionId,
|
||||||
|
ScalingBehavior = request.ScalingBehavior,
|
||||||
VideoFormat = request.VideoFormat,
|
VideoFormat = request.VideoFormat,
|
||||||
|
VideoProfile = request.VideoProfile,
|
||||||
|
VideoPreset = request.VideoPreset,
|
||||||
|
AllowBFrames = request.AllowBFrames,
|
||||||
BitDepth = request.BitDepth,
|
BitDepth = request.BitDepth,
|
||||||
VideoBitrate = request.VideoBitrate,
|
VideoBitrate = request.VideoBitrate,
|
||||||
VideoBufferSize = request.VideoBufferSize,
|
VideoBufferSize = request.VideoBufferSize,
|
||||||
|
TonemapAlgorithm = request.TonemapAlgorithm,
|
||||||
AudioFormat = request.AudioFormat,
|
AudioFormat = request.AudioFormat,
|
||||||
AudioBitrate = request.AudioBitrate,
|
AudioBitrate = request.AudioBitrate,
|
||||||
AudioBufferSize = request.AudioBufferSize,
|
AudioBufferSize = request.AudioBufferSize,
|
||||||
NormalizeLoudness = request.NormalizeLoudness,
|
NormalizeLoudnessMode = request.NormalizeLoudnessMode,
|
||||||
AudioChannels = request.AudioChannels,
|
AudioChannels = request.AudioChannels,
|
||||||
AudioSampleRate = request.AudioSampleRate,
|
AudioSampleRate = request.AudioSampleRate,
|
||||||
NormalizeFramerate = request.NormalizeFramerate,
|
NormalizeFramerate = request.NormalizeFramerate,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -9,23 +10,28 @@ namespace ErsatzTV.Application.FFmpegProfiles;
|
|||||||
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Unit>>
|
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Unit>>
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly ISearchTargets _searchTargets;
|
||||||
|
|
||||||
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||||
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_searchTargets = searchTargets;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Either<BaseError, Unit>> Handle(
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
DeleteFFmpegProfile request,
|
DeleteFFmpegProfile request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
|
Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
|
||||||
return await validation.Apply(p => DoDeletion(dbContext, p));
|
return await validation.Apply(p => DoDeletion(dbContext, p));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
|
private async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
|
||||||
{
|
{
|
||||||
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
|
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
_searchTargets.SearchTargetsChanged();
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,24 @@ public record UpdateFFmpegProfile(
|
|||||||
string Name,
|
string Name,
|
||||||
int ThreadCount,
|
int ThreadCount,
|
||||||
HardwareAccelerationKind HardwareAcceleration,
|
HardwareAccelerationKind HardwareAcceleration,
|
||||||
|
string VaapiDisplay,
|
||||||
VaapiDriver VaapiDriver,
|
VaapiDriver VaapiDriver,
|
||||||
string VaapiDevice,
|
string VaapiDevice,
|
||||||
int? QsvExtraHardwareFrames,
|
int? QsvExtraHardwareFrames,
|
||||||
int ResolutionId,
|
int ResolutionId,
|
||||||
|
ScalingBehavior ScalingBehavior,
|
||||||
FFmpegProfileVideoFormat VideoFormat,
|
FFmpegProfileVideoFormat VideoFormat,
|
||||||
|
string VideoProfile,
|
||||||
|
string VideoPreset,
|
||||||
|
bool AllowBFrames,
|
||||||
FFmpegProfileBitDepth BitDepth,
|
FFmpegProfileBitDepth BitDepth,
|
||||||
int VideoBitrate,
|
int VideoBitrate,
|
||||||
int VideoBufferSize,
|
int VideoBufferSize,
|
||||||
|
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
|
||||||
FFmpegProfileAudioFormat AudioFormat,
|
FFmpegProfileAudioFormat AudioFormat,
|
||||||
int AudioBitrate,
|
int AudioBitrate,
|
||||||
int AudioBufferSize,
|
int AudioBufferSize,
|
||||||
bool NormalizeLoudness,
|
NormalizeLoudnessMode NormalizeLoudnessMode,
|
||||||
int AudioChannels,
|
int AudioChannels,
|
||||||
int AudioSampleRate,
|
int AudioSampleRate,
|
||||||
bool NormalizeFramerate,
|
bool NormalizeFramerate,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -10,9 +11,13 @@ public class
|
|||||||
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
|
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly ISearchTargets _searchTargets;
|
||||||
|
|
||||||
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||||
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_searchTargets = searchTargets;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
|
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
|
||||||
UpdateFFmpegProfile request,
|
UpdateFFmpegProfile request,
|
||||||
@@ -20,7 +25,7 @@ public class
|
|||||||
{
|
{
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
|
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
|
||||||
return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request));
|
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
|
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
|
||||||
@@ -31,11 +36,16 @@ public class
|
|||||||
p.Name = update.Name;
|
p.Name = update.Name;
|
||||||
p.ThreadCount = update.ThreadCount;
|
p.ThreadCount = update.ThreadCount;
|
||||||
p.HardwareAcceleration = update.HardwareAcceleration;
|
p.HardwareAcceleration = update.HardwareAcceleration;
|
||||||
|
p.VaapiDisplay = update.VaapiDisplay;
|
||||||
p.VaapiDriver = update.VaapiDriver;
|
p.VaapiDriver = update.VaapiDriver;
|
||||||
p.VaapiDevice = update.VaapiDevice;
|
p.VaapiDevice = update.VaapiDevice;
|
||||||
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
|
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
|
||||||
p.ResolutionId = update.ResolutionId;
|
p.ResolutionId = update.ResolutionId;
|
||||||
|
p.ScalingBehavior = update.ScalingBehavior;
|
||||||
p.VideoFormat = update.VideoFormat;
|
p.VideoFormat = update.VideoFormat;
|
||||||
|
p.VideoProfile = update.VideoProfile;
|
||||||
|
p.VideoPreset = update.VideoPreset;
|
||||||
|
p.AllowBFrames = update.AllowBFrames;
|
||||||
|
|
||||||
// mpeg2video only supports 8-bit content
|
// mpeg2video only supports 8-bit content
|
||||||
p.BitDepth = update.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video
|
p.BitDepth = update.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video
|
||||||
@@ -44,15 +54,19 @@ public class
|
|||||||
|
|
||||||
p.VideoBitrate = update.VideoBitrate;
|
p.VideoBitrate = update.VideoBitrate;
|
||||||
p.VideoBufferSize = update.VideoBufferSize;
|
p.VideoBufferSize = update.VideoBufferSize;
|
||||||
|
p.TonemapAlgorithm = update.TonemapAlgorithm;
|
||||||
p.AudioFormat = update.AudioFormat;
|
p.AudioFormat = update.AudioFormat;
|
||||||
p.AudioBitrate = update.AudioBitrate;
|
p.AudioBitrate = update.AudioBitrate;
|
||||||
p.AudioBufferSize = update.AudioBufferSize;
|
p.AudioBufferSize = update.AudioBufferSize;
|
||||||
p.NormalizeLoudness = update.NormalizeLoudness;
|
p.NormalizeLoudnessMode = update.NormalizeLoudnessMode;
|
||||||
p.AudioChannels = update.AudioChannels;
|
p.AudioChannels = update.AudioChannels;
|
||||||
p.AudioSampleRate = update.AudioSampleRate;
|
p.AudioSampleRate = update.AudioSampleRate;
|
||||||
p.NormalizeFramerate = update.NormalizeFramerate;
|
p.NormalizeFramerate = update.NormalizeFramerate;
|
||||||
p.DeinterlaceVideo = update.DeinterlaceVideo;
|
p.DeinterlaceVideo = update.DeinterlaceVideo;
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
_searchTargets.SearchTargetsChanged();
|
||||||
|
|
||||||
return new UpdateFFmpegProfileResult(p.Id);
|
return new UpdateFFmpegProfileResult(p.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using ErsatzTV.Application.Subtitles;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
@@ -10,13 +13,16 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
|
|||||||
{
|
{
|
||||||
private readonly IConfigElementRepository _configElementRepository;
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
private readonly ILocalFileSystem _localFileSystem;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
|
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||||
|
|
||||||
public UpdateFFmpegSettingsHandler(
|
public UpdateFFmpegSettingsHandler(
|
||||||
IConfigElementRepository configElementRepository,
|
IConfigElementRepository configElementRepository,
|
||||||
ILocalFileSystem localFileSystem)
|
ILocalFileSystem localFileSystem,
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> workerChannel)
|
||||||
{
|
{
|
||||||
_configElementRepository = configElementRepository;
|
_configElementRepository = configElementRepository;
|
||||||
_localFileSystem = localFileSystem;
|
_localFileSystem = localFileSystem;
|
||||||
|
_workerChannel = workerChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle(
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
@@ -52,10 +58,8 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
|
|||||||
UseShellExecute = false
|
UseShellExecute = false
|
||||||
};
|
};
|
||||||
|
|
||||||
using var test = new Process
|
using var test = new Process();
|
||||||
{
|
test.StartInfo = startInfo;
|
||||||
StartInfo = startInfo
|
|
||||||
};
|
|
||||||
|
|
||||||
test.Start();
|
test.Start();
|
||||||
string output = await test.StandardOutput.ReadToEndAsync();
|
string output = await test.StandardOutput.ReadToEndAsync();
|
||||||
@@ -71,7 +75,7 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
|
|||||||
await _configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
|
await _configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
|
||||||
await _configElementRepository.Upsert(
|
await _configElementRepository.Upsert(
|
||||||
ConfigElementKey.FFmpegDefaultProfileId,
|
ConfigElementKey.FFmpegDefaultProfileId,
|
||||||
request.Settings.DefaultFFmpegProfileId.ToString());
|
request.Settings.DefaultFFmpegProfileId.ToString(CultureInfo.InvariantCulture));
|
||||||
await _configElementRepository.Upsert(
|
await _configElementRepository.Upsert(
|
||||||
ConfigElementKey.FFmpegSaveReports,
|
ConfigElementKey.FFmpegSaveReports,
|
||||||
request.Settings.SaveReports.ToString());
|
request.Settings.SaveReports.ToString());
|
||||||
@@ -88,6 +92,26 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
|
|||||||
ConfigElementKey.FFmpegPreferredLanguageCode,
|
ConfigElementKey.FFmpegPreferredLanguageCode,
|
||||||
request.Settings.PreferredAudioLanguageCode);
|
request.Settings.PreferredAudioLanguageCode);
|
||||||
|
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.FFmpegUseEmbeddedSubtitles,
|
||||||
|
request.Settings.UseEmbeddedSubtitles);
|
||||||
|
|
||||||
|
// do not extract when subtitles are not used
|
||||||
|
if (request.Settings.UseEmbeddedSubtitles == false)
|
||||||
|
{
|
||||||
|
request.Settings.ExtractEmbeddedSubtitles = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.FFmpegExtractEmbeddedSubtitles,
|
||||||
|
request.Settings.ExtractEmbeddedSubtitles);
|
||||||
|
|
||||||
|
// queue extracting all embedded subtitles
|
||||||
|
if (request.Settings.ExtractEmbeddedSubtitles)
|
||||||
|
{
|
||||||
|
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(Option<int>.None));
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Settings.GlobalWatermarkId is not null)
|
if (request.Settings.GlobalWatermarkId is not null)
|
||||||
{
|
{
|
||||||
await _configElementRepository.Upsert(
|
await _configElementRepository.Upsert(
|
||||||
|
|||||||
@@ -9,18 +9,24 @@ public record FFmpegProfileViewModel(
|
|||||||
string Name,
|
string Name,
|
||||||
int ThreadCount,
|
int ThreadCount,
|
||||||
HardwareAccelerationKind HardwareAcceleration,
|
HardwareAccelerationKind HardwareAcceleration,
|
||||||
|
string VaapiDisplay,
|
||||||
VaapiDriver VaapiDriver,
|
VaapiDriver VaapiDriver,
|
||||||
string VaapiDevice,
|
string VaapiDevice,
|
||||||
int? QsvExtraHardwareFrames,
|
int? QsvExtraHardwareFrames,
|
||||||
ResolutionViewModel Resolution,
|
ResolutionViewModel Resolution,
|
||||||
|
ScalingBehavior ScalingBehavior,
|
||||||
FFmpegProfileVideoFormat VideoFormat,
|
FFmpegProfileVideoFormat VideoFormat,
|
||||||
|
string VideoProfile,
|
||||||
|
string VideoPreset,
|
||||||
|
bool AllowBFrames,
|
||||||
FFmpegProfileBitDepth BitDepth,
|
FFmpegProfileBitDepth BitDepth,
|
||||||
int VideoBitrate,
|
int VideoBitrate,
|
||||||
int VideoBufferSize,
|
int VideoBufferSize,
|
||||||
|
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
|
||||||
FFmpegProfileAudioFormat AudioFormat,
|
FFmpegProfileAudioFormat AudioFormat,
|
||||||
int AudioBitrate,
|
int AudioBitrate,
|
||||||
int AudioBufferSize,
|
int AudioBufferSize,
|
||||||
bool NormalizeLoudness,
|
NormalizeLoudnessMode NormalizeLoudnessMode,
|
||||||
int AudioChannels,
|
int AudioChannels,
|
||||||
int AudioSampleRate,
|
int AudioSampleRate,
|
||||||
bool NormalizeFramerate,
|
bool NormalizeFramerate,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ public class FFmpegSettingsViewModel
|
|||||||
public string FFprobePath { get; set; }
|
public string FFprobePath { get; set; }
|
||||||
public int DefaultFFmpegProfileId { get; set; }
|
public int DefaultFFmpegProfileId { get; set; }
|
||||||
public string PreferredAudioLanguageCode { get; set; }
|
public string PreferredAudioLanguageCode { get; set; }
|
||||||
|
public bool UseEmbeddedSubtitles { get; set; }
|
||||||
|
public bool ExtractEmbeddedSubtitles { get; set; }
|
||||||
public bool SaveReports { get; set; }
|
public bool SaveReports { get; set; }
|
||||||
public int? GlobalWatermarkId { get; set; }
|
public int? GlobalWatermarkId { get; set; }
|
||||||
public int? GlobalFallbackFillerId { get; set; }
|
public int? GlobalFallbackFillerId { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using ErsatzTV.Application.Resolutions;
|
using ErsatzTV.Core.Api.FFmpegProfiles;
|
||||||
using ErsatzTV.Core.Api.FFmpegProfiles;
|
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||||
@@ -12,18 +11,24 @@ internal static class Mapper
|
|||||||
profile.Name,
|
profile.Name,
|
||||||
profile.ThreadCount,
|
profile.ThreadCount,
|
||||||
profile.HardwareAcceleration,
|
profile.HardwareAcceleration,
|
||||||
|
profile.VaapiDisplay ?? "drm",
|
||||||
profile.VaapiDriver,
|
profile.VaapiDriver,
|
||||||
profile.VaapiDevice,
|
profile.VaapiDevice,
|
||||||
profile.QsvExtraHardwareFrames,
|
profile.QsvExtraHardwareFrames,
|
||||||
Project(profile.Resolution),
|
Resolutions.Mapper.ProjectToViewModel(profile.Resolution),
|
||||||
|
profile.ScalingBehavior,
|
||||||
profile.VideoFormat,
|
profile.VideoFormat,
|
||||||
|
profile.VideoProfile,
|
||||||
|
profile.VideoPreset ?? string.Empty,
|
||||||
|
profile.AllowBFrames,
|
||||||
profile.BitDepth,
|
profile.BitDepth,
|
||||||
profile.VideoBitrate,
|
profile.VideoBitrate,
|
||||||
profile.VideoBufferSize,
|
profile.VideoBufferSize,
|
||||||
|
profile.TonemapAlgorithm,
|
||||||
profile.AudioFormat,
|
profile.AudioFormat,
|
||||||
profile.AudioBitrate,
|
profile.AudioBitrate,
|
||||||
profile.AudioBufferSize,
|
profile.AudioBufferSize,
|
||||||
profile.NormalizeLoudness,
|
profile.NormalizeLoudnessMode,
|
||||||
profile.AudioChannels,
|
profile.AudioChannels,
|
||||||
profile.AudioSampleRate,
|
profile.AudioSampleRate,
|
||||||
profile.NormalizeFramerate,
|
profile.NormalizeFramerate,
|
||||||
@@ -43,21 +48,20 @@ internal static class Mapper
|
|||||||
ffmpegProfile.Name,
|
ffmpegProfile.Name,
|
||||||
ffmpegProfile.ThreadCount,
|
ffmpegProfile.ThreadCount,
|
||||||
(int)ffmpegProfile.HardwareAcceleration,
|
(int)ffmpegProfile.HardwareAcceleration,
|
||||||
|
ffmpegProfile.VaapiDisplay,
|
||||||
(int)ffmpegProfile.VaapiDriver,
|
(int)ffmpegProfile.VaapiDriver,
|
||||||
ffmpegProfile.VaapiDevice,
|
ffmpegProfile.VaapiDevice,
|
||||||
ffmpegProfile.ResolutionId,
|
ffmpegProfile.ResolutionId,
|
||||||
(int)ffmpegProfile.VideoFormat,
|
(int)ffmpegProfile.VideoFormat,
|
||||||
ffmpegProfile.VideoBitrate,
|
ffmpegProfile.VideoBitrate,
|
||||||
ffmpegProfile.VideoBufferSize,
|
ffmpegProfile.VideoBufferSize,
|
||||||
|
(int)ffmpegProfile.TonemapAlgorithm,
|
||||||
(int)ffmpegProfile.AudioFormat,
|
(int)ffmpegProfile.AudioFormat,
|
||||||
ffmpegProfile.AudioBitrate,
|
ffmpegProfile.AudioBitrate,
|
||||||
ffmpegProfile.AudioBufferSize,
|
ffmpegProfile.AudioBufferSize,
|
||||||
ffmpegProfile.NormalizeLoudness,
|
(int)ffmpegProfile.NormalizeLoudnessMode,
|
||||||
ffmpegProfile.AudioChannels,
|
ffmpegProfile.AudioChannels,
|
||||||
ffmpegProfile.AudioSampleRate,
|
ffmpegProfile.AudioSampleRate,
|
||||||
ffmpegProfile.NormalizeFramerate,
|
ffmpegProfile.NormalizeFramerate,
|
||||||
ffmpegProfile.DeinterlaceVideo);
|
ffmpegProfile.DeinterlaceVideo);
|
||||||
|
|
||||||
private static ResolutionViewModel Project(Resolution resolution) =>
|
|
||||||
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles,
|
|||||||
GetAllFFmpegProfiles request,
|
GetAllFFmpegProfiles request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
return await dbContext.FFmpegProfiles
|
return await dbContext.FFmpegProfiles
|
||||||
.Include(p => p.Resolution)
|
.Include(p => p.Resolution)
|
||||||
.ToListAsync(cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpe
|
|||||||
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
|
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
|
||||||
Option<string> preferredAudioLanguageCode =
|
Option<string> preferredAudioLanguageCode =
|
||||||
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||||
|
Option<bool> useEmbeddedSubtitles =
|
||||||
|
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegUseEmbeddedSubtitles);
|
||||||
|
Option<bool> extractEmbeddedSubtitles =
|
||||||
|
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegExtractEmbeddedSubtitles);
|
||||||
Option<int> watermark =
|
Option<int> watermark =
|
||||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||||
Option<int> fallbackFiller =
|
Option<int> fallbackFiller =
|
||||||
@@ -42,6 +46,8 @@ public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpe
|
|||||||
FFprobePath = await ffprobePath.IfNoneAsync(string.Empty),
|
FFprobePath = await ffprobePath.IfNoneAsync(string.Empty),
|
||||||
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
|
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
|
||||||
SaveReports = await saveReports.IfNoneAsync(false),
|
SaveReports = await saveReports.IfNoneAsync(false),
|
||||||
|
UseEmbeddedSubtitles = await useEmbeddedSubtitles.IfNoneAsync(true),
|
||||||
|
ExtractEmbeddedSubtitles = await extractEmbeddedSubtitles.IfNoneAsync(false),
|
||||||
PreferredAudioLanguageCode = await preferredAudioLanguageCode.IfNoneAsync("eng"),
|
PreferredAudioLanguageCode = await preferredAudioLanguageCode.IfNoneAsync("eng"),
|
||||||
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
|
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
|
||||||
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
|
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||||
|
|
||||||
|
public record GetSupportedHardwareAccelerationKinds : IRequest<List<HardwareAccelerationKind>>;
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.FFmpeg;
|
||||||
|
using ErsatzTV.FFmpeg.Capabilities;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||||
|
|
||||||
|
public class
|
||||||
|
GetSupportedHardwareAccelerationKindsHandler : IRequestHandler<GetSupportedHardwareAccelerationKinds,
|
||||||
|
List<HardwareAccelerationKind>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
|
||||||
|
|
||||||
|
public GetSupportedHardwareAccelerationKindsHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<HardwareAccelerationKind>> Handle(
|
||||||
|
GetSupportedHardwareAccelerationKinds request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
Validation<BaseError, string> validation = await Validate(dbContext);
|
||||||
|
|
||||||
|
return await validation.Match(
|
||||||
|
GetHardwareAccelerationKinds,
|
||||||
|
_ => Task.FromResult(new List<HardwareAccelerationKind> { HardwareAccelerationKind.None }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<HardwareAccelerationKind>> GetHardwareAccelerationKinds(string ffmpegPath)
|
||||||
|
{
|
||||||
|
var result = new List<HardwareAccelerationKind> { HardwareAccelerationKind.None };
|
||||||
|
|
||||||
|
IFFmpegCapabilities ffmpegCapabilities = await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
|
||||||
|
|
||||||
|
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Nvenc))
|
||||||
|
{
|
||||||
|
result.Add(HardwareAccelerationKind.Nvenc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Qsv))
|
||||||
|
{
|
||||||
|
result.Add(HardwareAccelerationKind.Qsv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Vaapi))
|
||||||
|
{
|
||||||
|
result.Add(HardwareAccelerationKind.Vaapi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.VideoToolbox))
|
||||||
|
{
|
||||||
|
result.Add(HardwareAccelerationKind.VideoToolbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Amf))
|
||||||
|
{
|
||||||
|
result.Add(HardwareAccelerationKind.Amf);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, string>> Validate(TvContext dbContext) =>
|
||||||
|
await FFmpegPathMustExist(dbContext);
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) =>
|
||||||
|
dbContext.ConfigElements.GetValue<string>(ConfigElementKey.FFmpegPath)
|
||||||
|
.FilterT(File.Exists)
|
||||||
|
.Map(maybePath => maybePath.ToValidation<BaseError>("FFmpeg path does not exist on filesystem"));
|
||||||
|
}
|
||||||
@@ -16,5 +16,6 @@ public record CreateFillerPreset(
|
|||||||
int? CollectionId,
|
int? CollectionId,
|
||||||
int? MediaItemId,
|
int? MediaItemId,
|
||||||
int? MultiCollectionId,
|
int? MultiCollectionId,
|
||||||
int? SmartCollectionId
|
int? SmartCollectionId,
|
||||||
|
string Expression
|
||||||
) : IRequest<Either<BaseError, Unit>>;
|
) : IRequest<Either<BaseError, Unit>>;
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Eit
|
|||||||
CollectionId = request.CollectionId,
|
CollectionId = request.CollectionId,
|
||||||
MediaItemId = request.MediaItemId,
|
MediaItemId = request.MediaItemId,
|
||||||
MultiCollectionId = request.MultiCollectionId,
|
MultiCollectionId = request.MultiCollectionId,
|
||||||
SmartCollectionId = request.SmartCollectionId
|
SmartCollectionId = request.SmartCollectionId,
|
||||||
|
Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null
|
||||||
};
|
};
|
||||||
|
|
||||||
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);
|
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class DeleteFillerPresetHandler : IRequestHandler<DeleteFillerPreset, Eit
|
|||||||
DeleteFillerPreset request,
|
DeleteFillerPreset request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
|
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
|
||||||
return await validation.Apply(ps => DoDeletion(dbContext, ps));
|
return await validation.Apply(ps => DoDeletion(dbContext, ps));
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ public class DeleteFillerPresetHandler : IRequestHandler<DeleteFillerPreset, Eit
|
|||||||
return dbContext.SaveChangesAsync().ToUnit();
|
return dbContext.SaveChangesAsync().ToUnit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
|
private static Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
|
||||||
TvContext dbContext,
|
TvContext dbContext,
|
||||||
DeleteFillerPreset request) =>
|
DeleteFillerPreset request) =>
|
||||||
dbContext.FillerPresets
|
dbContext.FillerPresets
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ public record UpdateFillerPreset(
|
|||||||
int? CollectionId,
|
int? CollectionId,
|
||||||
int? MediaItemId,
|
int? MediaItemId,
|
||||||
int? MultiCollectionId,
|
int? MultiCollectionId,
|
||||||
int? SmartCollectionId
|
int? SmartCollectionId,
|
||||||
|
string Expression
|
||||||
) : IRequest<Either<BaseError, Unit>>;
|
) : IRequest<Either<BaseError, Unit>>;
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Eit
|
|||||||
{
|
{
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
|
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
|
||||||
return await LanguageExtensions.Apply(validation, ps => ApplyUpdateRequest(dbContext, ps, request));
|
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Unit> ApplyUpdateRequest(
|
private static async Task<Unit> ApplyUpdateRequest(
|
||||||
TvContext dbContext,
|
TvContext dbContext,
|
||||||
FillerPreset existing,
|
FillerPreset existing,
|
||||||
UpdateFillerPreset request)
|
UpdateFillerPreset request)
|
||||||
@@ -37,6 +37,7 @@ public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Eit
|
|||||||
existing.MediaItemId = request.MediaItemId;
|
existing.MediaItemId = request.MediaItemId;
|
||||||
existing.MultiCollectionId = request.MultiCollectionId;
|
existing.MultiCollectionId = request.MultiCollectionId;
|
||||||
existing.SmartCollectionId = request.SmartCollectionId;
|
existing.SmartCollectionId = request.SmartCollectionId;
|
||||||
|
existing.Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null;
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user