mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
67 Commits
v1.24.3.47
...
v1.26.0.48
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94f439e238 | ||
|
|
903a88c121 | ||
|
|
9690ab6883 | ||
|
|
1e1a2b3b4a | ||
|
|
9dc2d3669c | ||
|
|
511c76e219 | ||
|
|
78329b7b92 | ||
|
|
4240048853 | ||
|
|
432af42ffd | ||
|
|
0d6c03f8d4 | ||
|
|
96830f975e | ||
|
|
13c538ff58 | ||
|
|
14250e9634 | ||
|
|
e2f7890d76 | ||
|
|
257d38de66 | ||
|
|
fd2a14e01b | ||
|
|
b4d76c7138 | ||
|
|
9655f37fa8 | ||
|
|
246fb9b855 | ||
|
|
25afadc9b2 | ||
|
|
3f547f0856 | ||
|
|
11e322b6d7 | ||
|
|
02ff133a62 | ||
|
|
47268aac87 | ||
|
|
8aad1ac554 | ||
|
|
9037cde439 | ||
|
|
2afafd79e4 | ||
|
|
f4fa2517d2 | ||
|
|
37bc46c1cd | ||
|
|
3e3a7ed4f0 | ||
|
|
04fa7d366d | ||
|
|
ed9a3214a2 | ||
|
|
66a9e1a653 | ||
|
|
8cb59c35fb | ||
|
|
94e9c05d60 | ||
|
|
8d2c4e1246 | ||
|
|
c05be39346 | ||
|
|
951d42a591 | ||
|
|
dd046d8a68 | ||
|
|
efa54a4d51 | ||
|
|
3f07c50cc5 | ||
|
|
94cf07ddb4 | ||
|
|
24063e06ab | ||
|
|
e8ebb87189 | ||
|
|
896e196767 | ||
|
|
9f5be75e6d | ||
|
|
9cc9e720bb | ||
|
|
a9c2cca66d | ||
|
|
9cc3646be5 | ||
|
|
d6bca449da | ||
|
|
cb5764c654 | ||
|
|
19a9b56fa4 | ||
|
|
a2b0f199f1 | ||
|
|
59bfad7614 | ||
|
|
aee3f2d12b | ||
|
|
11d58b4460 | ||
|
|
ee4de6c6ca | ||
|
|
8d16b88185 | ||
|
|
121ef8e80d | ||
|
|
d53fec7e75 | ||
|
|
c017a3cd7e | ||
|
|
27ea93090f | ||
|
|
d79845144e | ||
|
|
3f77900dd0 | ||
|
|
4e8b9e81cf | ||
|
|
a32ab3acfd | ||
|
|
942da3a5c0 |
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1.3318" y1="43.7371" x2="67.0419" y2="26.0967">
|
||||
<stop offset="0.1237" style="stop-color:#7866FF"/>
|
||||
<stop offset="0.5376" style="stop-color:#FE2EB6"/>
|
||||
<stop offset="0.8548" style="stop-color:#FD0486"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_1_);" points="67.3,16 43.7,0 0,31.1 11.1,70 58.9,60.3 "/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="45.9148" y1="38.9098" x2="67.6577" y2="9.0989">
|
||||
<stop offset="0.1237" style="stop-color:#FF0080"/>
|
||||
<stop offset="0.2587" style="stop-color:#FE0385"/>
|
||||
<stop offset="0.4109" style="stop-color:#FA0C92"/>
|
||||
<stop offset="0.5713" style="stop-color:#F41BA9"/>
|
||||
<stop offset="0.7363" style="stop-color:#EB2FC8"/>
|
||||
<stop offset="0.8656" style="stop-color:#E343E6"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_2_);" points="67.3,16 43.7,0 38,15.7 38,47.8 70,47.8 "/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
|
||||
<rect x="17.4" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M17.4,19.1h6.9c5.6,0,9.5,3.8,9.5,8.9V28c0,5-3.9,8.9-9.5,8.9h-6.9V19.1z M21.4,22.7v10.7h3
|
||||
c3.2,0,5.4-2.2,5.4-5.3V28c0-3.2-2.2-5.4-5.4-5.4H21.4z"/>
|
||||
<polygon style="fill:#FFFFFF;" points="40.3,22.7 34.9,22.7 34.9,19.1 49.6,19.1 49.6,22.7 44.2,22.7 44.2,37 40.3,37 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,66 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24">
|
||||
<stop offset="0" style="stop-color:#FCEE39"/>
|
||||
<stop offset="1" style="stop-color:#F37B3D"/>
|
||||
</linearGradient>
|
||||
<path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9
|
||||
c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0
|
||||
c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/>
|
||||
<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546">
|
||||
<stop offset="0" style="stop-color:#EF5A6B"/>
|
||||
<stop offset="0.57" style="stop-color:#F26F4E"/>
|
||||
<stop offset="1" style="stop-color:#F37B3D"/>
|
||||
</linearGradient>
|
||||
<path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0
|
||||
c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3
|
||||
c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/>
|
||||
<linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562">
|
||||
<stop offset="0" style="stop-color:#7C59A4"/>
|
||||
<stop offset="0.3852" style="stop-color:#AF4C92"/>
|
||||
<stop offset="0.7654" style="stop-color:#DC4183"/>
|
||||
<stop offset="0.957" style="stop-color:#ED3D7D"/>
|
||||
</linearGradient>
|
||||
<path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9
|
||||
c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0
|
||||
c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/>
|
||||
<linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971">
|
||||
<stop offset="0" style="stop-color:#EF5A6B"/>
|
||||
<stop offset="0.364" style="stop-color:#EE4E72"/>
|
||||
<stop offset="1" style="stop-color:#ED3D7D"/>
|
||||
</linearGradient>
|
||||
<path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0
|
||||
l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0
|
||||
c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/>
|
||||
<g id="XMLID_3008_">
|
||||
<rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/>
|
||||
<rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/>
|
||||
<g id="XMLID_3009_">
|
||||
<path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0
|
||||
l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/>
|
||||
<path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0
|
||||
L45.3,43.8z"/>
|
||||
<path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/>
|
||||
<path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0
|
||||
c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5
|
||||
l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/>
|
||||
<path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0
|
||||
c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1
|
||||
l-1.5,0v2H50.6z"/>
|
||||
<path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z
|
||||
M58.8,59l-0.9-2.3L57,59L58.8,59z"/>
|
||||
<path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/>
|
||||
<path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z"
|
||||
/>
|
||||
<path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0
|
||||
c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6
|
||||
c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7
|
||||
C76.1,62.5,74.7,62,73.7,61.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="22.9451" y1="75.7869" x2="74.7868" y2="20.6415">
|
||||
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
||||
<stop offset="0.4044" style="stop-color:#C41E57"/>
|
||||
<stop offset="0.4677" style="stop-color:#C41E57"/>
|
||||
<stop offset="0.6505" style="stop-color:#EB8523"/>
|
||||
<stop offset="0.9516" style="stop-color:#FEBD11"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_1_);" points="49.8,15.2 36,36.7 58.4,70 70,23.1 "/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.7187" y1="73.2922" x2="69.5556" y2="18.1519">
|
||||
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
||||
<stop offset="0.4044" style="stop-color:#C41E57"/>
|
||||
<stop offset="0.4677" style="stop-color:#C41E57"/>
|
||||
<stop offset="0.7043" style="stop-color:#EB8523"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_2_);" points="51.1,15.7 49,0 18.8,33.6 27.6,42.3 20.8,70 58.4,70 "/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1.8281" y1="53.4275" x2="48.8245" y2="9.2255">
|
||||
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
||||
<stop offset="0.6613" style="stop-color:#C41E57"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_3_);" points="49,0 11.6,0 0,47.1 55.6,47.1 "/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="49.8935" y1="-11.5569" x2="48.8588" y2="24.0352">
|
||||
<stop offset="0.5" style="stop-color:#C41E57"/>
|
||||
<stop offset="0.6668" style="stop-color:#D13F48"/>
|
||||
<stop offset="0.7952" style="stop-color:#D94F39"/>
|
||||
<stop offset="0.8656" style="stop-color:#DD5433"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_4_);" points="55.3,47.1 51.1,15.7 49,0 41.7,23 "/>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<rect x="13.4" y="13.5" transform="matrix(-1 2.577289e-003 -2.577289e-003 -1 70.0288 70.081)" style="fill:#000000;" width="43.2" height="43.2"/>
|
||||
|
||||
<rect x="17.6" y="48.6" transform="matrix(1 -2.577289e-003 2.577289e-003 1 -0.1287 6.634109e-002)" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
||||
<path style="fill:#FFFFFF;" d="M17.4,19.1l8.2,0c2.3,0,4,0.6,5.2,1.8c1,1,1.5,2.4,1.5,4.1l0,0.1c0,1.5-0.3,2.6-1.1,3.5
|
||||
c-0.7,0.9-1.6,1.6-2.8,2l4.4,6.4l-4.6,0l-3.7-5.5l-3.3,0l0,5.5l-3.9,0L17.4,19.1z M25.3,27.8c1,0,1.7-0.2,2.2-0.7
|
||||
c0.5-0.5,0.8-1.1,0.8-1.8l0-0.1c0-0.9-0.3-1.5-0.8-1.9c-0.5-0.4-1.3-0.6-2.3-0.6l-3.9,0l0,5.1L25.3,27.8z"/>
|
||||
<path style="fill:#FFFFFF;" d="M36,33.2l-1.9,0l0-3.3l2.5,0l0.6-3.8l-2.3,0l0-3.3l2.8,0l0.6-3.7l3.4,0l-0.6,3.7l3.7,0l0.6-3.7
|
||||
l3.4,0l-0.6,3.7l1.9,0l0,3.3l-2.5,0L47,29.9l2.3,0l0,3.3l-2.8,0L45.8,37l-3.4,0l0.7-3.8l-3.7,0L38.7,37l-3.4,0L36,33.2z
|
||||
M43.7,29.9l0.6-3.8l-3.7,0L40,29.9L43.7,29.9z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
||||
<defs>
|
||||
<linearGradient id="linear-gradient" x1="70.22612" y1="27.79912" x2="-5.13024" y2="63.12242" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#c90f5e"/>
|
||||
<stop offset="0.22111" stop-color="#c90f5e"/>
|
||||
<stop offset="0.2356" stop-color="#c90f5e"/>
|
||||
<stop offset="0.35559" stop-color="#ca135c"/>
|
||||
<stop offset="0.46633" stop-color="#ce1e57"/>
|
||||
<stop offset="0.5735" stop-color="#d4314e"/>
|
||||
<stop offset="0.67844" stop-color="#dc4b41"/>
|
||||
<stop offset="0.78179" stop-color="#e66d31"/>
|
||||
<stop offset="0.88253" stop-color="#f3961d"/>
|
||||
<stop offset="0.94241" stop-color="#fcb20f"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-2" x1="24.65904" y1="61.99608" x2="46.04762" y2="2.93445" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.04188" stop-color="#077cfb"/>
|
||||
<stop offset="0.44503" stop-color="#c90f5e"/>
|
||||
<stop offset="0.95812" stop-color="#077cfb"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-3" x1="17.39552" y1="63.34592" x2="33.19389" y2="7.20092" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.27749" stop-color="#c90f5e"/>
|
||||
<stop offset="0.97382" stop-color="#fcb20f"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<title>rider</title>
|
||||
<g>
|
||||
<polygon points="70 27.237 63.391 23.75 20.926 0 3.827 17.921 21.619 41.068 60.537 44.397 70 27.237" fill="url(#linear-gradient)"/>
|
||||
<polygon points="50.423 16.132 44.271 1.107 27.643 17.471 11.768 50.194 49.411 70 70 57.98 50.423 16.132" fill="url(#linear-gradient-2)"/>
|
||||
<polygon points="20.926 0 0 14.095 7.779 62.172 27.848 69.889 53.78 48.823 20.926 0" fill="url(#linear-gradient-3)"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="13.30219" y="13.19311" width="43.61371" height="43.61371"/>
|
||||
<g>
|
||||
<path d="M17.22741,18.86293h8.39564a7.38416,7.38416,0,0,1,5.34268,1.85358,5.86989,5.86989,0,0,1,1.52648,4.1433h0A5.74339,5.74339,0,0,1,28.567,30.5296l4.47041,6.54206H28.34891L24.42368,31.1838h-3.162v5.88785H17.22741V18.86293h0ZM25.296,27.69471c1.96262,0,3.053-1.09034,3.053-2.61682h0c0-1.74455-1.19938-2.61682-3.162-2.61682H21.15265v5.23365H25.296Z" fill="#fff"/>
|
||||
<path d="M36.09034,18.86293H43.2866c5.77882,0,9.70405,3.92523,9.70405,9.15888h0c0,5.12461-3.92523,9.15888-9.70405,9.15888H36.09034V18.86293Zm4.03427,3.59813V33.47352h3.162a5.23727,5.23727,0,0,0,5.56075-5.45171h0a5.26493,5.26493,0,0,0-5.56075-5.56075h-3.162Z" fill="#fff"/>
|
||||
</g>
|
||||
<rect x="17.22741" y="48.62925" width="16.35514" height="2.72586" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="25.0676" y1="1.4599" x2="43.1829" y2="66.675">
|
||||
<stop offset="0.2849" style="stop-color:#00CDD7"/>
|
||||
<stop offset="0.9409" style="stop-color:#2086D7"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_1_);" points="9.4,63.3 0,7.3 17.5,0.1 28.6,6.7 38.8,1.2 60.1,9.4 48.1,70 "/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="30.7199" y1="9.7343" x2="61.365" y2="54.6713">
|
||||
<stop offset="0.1398" style="stop-color:#FFF045"/>
|
||||
<stop offset="0.3656" style="stop-color:#00CDD7"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_2_);" points="70,23.7 61,1.4 44.6,0 19.3,24.3 26.1,55.6 38.8,64.6 70,46 62.3,31.7 "/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="61.0819" y1="15.2899" x2="65.1065" y2="29.5436">
|
||||
<stop offset="0.2849" style="stop-color:#00CDD7"/>
|
||||
<stop offset="0.9409" style="stop-color:#2086D7"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_3_);" points="56,20.4 62.3,31.7 70,23.7 64.4,9.8 "/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
|
||||
<rect x="17.5" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
||||
<path style="fill:#FFFFFF;" d="M38.7,34.3l2.3-2.8c1.6,1.3,3.3,2.2,5.3,2.2c1.6,0,2.5-0.6,2.5-1.7v-0.1c0-1-0.6-1.5-3.6-2.3
|
||||
c-3.6-0.9-5.8-1.9-5.8-5.5v-0.1c0-3.3,2.6-5.4,6.2-5.4c2.6,0,4.8,0.8,6.6,2.3l-2,3c-1.6-1.1-3.1-1.8-4.6-1.8
|
||||
c-1.5,0-2.3,0.7-2.3,1.6v0.1c0,1.2,0.8,1.6,3.8,2.4c3.6,1,5.6,2.3,5.6,5.4v0.1c0,3.6-2.7,5.6-6.5,5.6
|
||||
C43.5,37.2,40.8,36.2,38.7,34.3"/>
|
||||
</g>
|
||||
<polygon style="fill:#FFFFFF;" points="35.2,19 32.5,29.4 29.5,19 26.5,19 23.4,29.4 20.7,19 16.6,19 21.7,36.9 25,36.9 28,26.5
|
||||
30.9,36.9 34.3,36.9 39.4,19 "/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
12
README.md
12
README.md
@@ -68,16 +68,16 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
|
||||
|
||||
## JetBrains
|
||||
|
||||
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
||||
Thank you to [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains" width="96">](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
||||
|
||||
- [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||
- [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
- [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
- [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/ReSharper_icon.png" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/WebStorm_icon.png" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/Rider_icon.png" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/dotTrace_icon.png" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||
|
||||
### License
|
||||
|
||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
- Copyright 2010-2022
|
||||
- Copyright 2010-2024
|
||||
|
||||
Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box)
|
||||
|
||||
@@ -9,18 +9,18 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.24.3'
|
||||
majorVersion: '1.26.0'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.424'
|
||||
dotnetVersion: '6.0.427'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-12'
|
||||
macImage: 'macOS-13'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
|
||||
@@ -20,7 +20,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
|
||||
function RedirectWithUrlBase() {
|
||||
@@ -99,7 +99,7 @@ function AppRoutes() {
|
||||
|
||||
<Route path="/system/backup" component={BackupsConnector} />
|
||||
|
||||
<Route path="/system/updates" component={UpdatesConnector} />
|
||||
<Route path="/system/updates" component={Updates} />
|
||||
|
||||
<Route path="/system/events" component={LogsTableConnector} />
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import { IndexerCategory } from 'Indexer/Indexer';
|
||||
import Application from 'typings/Application';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import Notification from 'typings/Notification';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
|
||||
export interface AppProfileAppState
|
||||
extends AppSectionState<Application>,
|
||||
@@ -28,6 +29,10 @@ export interface DownloadClientAppState
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface GeneralAppState
|
||||
extends AppSectionItemState<General>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface IndexerCategoryAppState
|
||||
extends AppSectionState<IndexerCategory>,
|
||||
AppSectionDeleteState,
|
||||
@@ -43,6 +48,7 @@ interface SettingsAppState {
|
||||
appProfiles: AppProfileAppState;
|
||||
applications: ApplicationAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
indexerCategories: IndexerCategoryAppState;
|
||||
notifications: NotificationAppState;
|
||||
ui: UiSettingsAppState;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './FormInputButton.css';
|
||||
|
||||
function FormInputButton(props) {
|
||||
const {
|
||||
className,
|
||||
canSpin,
|
||||
isLastButton,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
if (canSpin) {
|
||||
return (
|
||||
<SpinnerButton
|
||||
className={classNames(
|
||||
className,
|
||||
!isLastButton && styles.middleButton
|
||||
)}
|
||||
kind={kinds.PRIMARY}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={classNames(
|
||||
className,
|
||||
!isLastButton && styles.middleButton
|
||||
)}
|
||||
kind={kinds.PRIMARY}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
FormInputButton.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
isLastButton: PropTypes.bool.isRequired,
|
||||
canSpin: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
FormInputButton.defaultProps = {
|
||||
className: styles.button,
|
||||
isLastButton: true,
|
||||
canSpin: false
|
||||
};
|
||||
|
||||
export default FormInputButton;
|
||||
38
frontend/src/Components/Form/FormInputButton.tsx
Normal file
38
frontend/src/Components/Form/FormInputButton.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Button, { ButtonProps } from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './FormInputButton.css';
|
||||
|
||||
export interface FormInputButtonProps extends ButtonProps {
|
||||
canSpin?: boolean;
|
||||
isLastButton?: boolean;
|
||||
}
|
||||
|
||||
function FormInputButton({
|
||||
className = styles.button,
|
||||
canSpin = false,
|
||||
isLastButton = true,
|
||||
...otherProps
|
||||
}: FormInputButtonProps) {
|
||||
if (canSpin) {
|
||||
return (
|
||||
<SpinnerButton
|
||||
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||
kind={kinds.PRIMARY}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||
kind={kinds.PRIMARY}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormInputButton;
|
||||
@@ -1,48 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import styles from './Label.css';
|
||||
|
||||
function Label(props) {
|
||||
const {
|
||||
className,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
children,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Label.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
Label.defaultProps = {
|
||||
className: styles.label,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.SMALL,
|
||||
outline: false
|
||||
};
|
||||
|
||||
export default Label;
|
||||
33
frontend/src/Components/Label.tsx
Normal file
33
frontend/src/Components/Label.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import styles from './Label.css';
|
||||
|
||||
export interface LabelProps extends ComponentProps<'span'> {
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
outline?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Label({
|
||||
className = styles.label,
|
||||
kind = kinds.DEFAULT,
|
||||
size = sizes.SMALL,
|
||||
outline = false,
|
||||
...otherProps
|
||||
}: LabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { align, kinds, sizes } from 'Helpers/Props';
|
||||
import Link from './Link';
|
||||
import styles from './Button.css';
|
||||
|
||||
class Button extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
buttonGroupPosition,
|
||||
kind,
|
||||
size,
|
||||
children,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
buttonGroupPosition && styles[buttonGroupPosition]
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
buttonGroupPosition: PropTypes.oneOf(align.all),
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
size: PropTypes.oneOf(sizes.all),
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
className: styles.button,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.MEDIUM
|
||||
};
|
||||
|
||||
export default Button;
|
||||
37
frontend/src/Components/Link/Button.tsx
Normal file
37
frontend/src/Components/Link/Button.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { align, kinds, sizes } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import Link, { LinkProps } from './Link';
|
||||
import styles from './Button.css';
|
||||
|
||||
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
|
||||
buttonGroupPosition?: Extract<
|
||||
(typeof align.all)[number],
|
||||
keyof typeof styles
|
||||
>;
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
children: Required<LinkProps['children']>;
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
className = styles.button,
|
||||
buttonGroupPosition,
|
||||
kind = kinds.DEFAULT,
|
||||
size = sizes.MEDIUM,
|
||||
...otherProps
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
buttonGroupPosition && styles[buttonGroupPosition]
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import Clipboard from 'clipboard';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import styles from './ClipboardButton.css';
|
||||
|
||||
class ClipboardButton extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._id = getUniqueElememtId();
|
||||
this._successTimeout = null;
|
||||
this._testResultTimeout = null;
|
||||
|
||||
this.state = {
|
||||
showSuccess: false,
|
||||
showError: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._clipboard = new Clipboard(`#${this._id}`, {
|
||||
text: () => this.props.value,
|
||||
container: document.getElementById(this._id)
|
||||
});
|
||||
|
||||
this._clipboard.on('success', this.onSuccess);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
showSuccess,
|
||||
showError
|
||||
} = this.state;
|
||||
|
||||
if (showSuccess || showError) {
|
||||
this._testResultTimeout = setTimeout(this.resetState, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._clipboard) {
|
||||
this._clipboard.destroy();
|
||||
}
|
||||
|
||||
if (this._testResultTimeout) {
|
||||
clearTimeout(this._testResultTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = () => {
|
||||
this.setState({
|
||||
showSuccess: false,
|
||||
showError: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSuccess = () => {
|
||||
this.setState({
|
||||
showSuccess: true
|
||||
});
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
this.setState({
|
||||
showError: true
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
className,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
showSuccess,
|
||||
showError
|
||||
} = this.state;
|
||||
|
||||
const showStateIcon = showSuccess || showError;
|
||||
const iconName = showError ? icons.DANGER : icons.CHECK;
|
||||
const iconKind = showError ? kinds.DANGER : kinds.SUCCESS;
|
||||
|
||||
return (
|
||||
<FormInputButton
|
||||
id={this._id}
|
||||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
<span className={showStateIcon ? styles.showStateIcon : undefined}>
|
||||
{
|
||||
showSuccess &&
|
||||
<span className={styles.stateIconContainer}>
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
<span className={styles.clipboardIconContainer}>
|
||||
<Icon name={icons.CLIPBOARD} />
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</FormInputButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClipboardButton.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
ClipboardButton.defaultProps = {
|
||||
className: styles.button
|
||||
};
|
||||
|
||||
export default ClipboardButton;
|
||||
76
frontend/src/Components/Link/ClipboardButton.tsx
Normal file
76
frontend/src/Components/Link/ClipboardButton.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import copy from 'copy-to-clipboard';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { ButtonProps } from './Button';
|
||||
import styles from './ClipboardButton.css';
|
||||
|
||||
export interface ClipboardButtonProps extends Omit<ButtonProps, 'children'> {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ClipboardState = 'success' | 'error' | null;
|
||||
|
||||
export default function ClipboardButton({
|
||||
id,
|
||||
value,
|
||||
className = styles.button,
|
||||
...otherProps
|
||||
}: ClipboardButtonProps) {
|
||||
const [state, setState] = useState<ClipboardState>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
setState(null);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
try {
|
||||
if ('clipboard' in navigator) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
} else {
|
||||
copy(value);
|
||||
}
|
||||
|
||||
setState('success');
|
||||
} catch (e) {
|
||||
setState('error');
|
||||
console.error(`Failed to copy to clipboard`, e);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<FormInputButton
|
||||
className={className}
|
||||
onClick={handleClick}
|
||||
{...otherProps}
|
||||
>
|
||||
<span className={state ? styles.showStateIcon : undefined}>
|
||||
{state ? (
|
||||
<span className={styles.stateIconContainer}>
|
||||
<Icon
|
||||
name={state === 'error' ? icons.DANGER : icons.CHECK}
|
||||
kind={state === 'error' ? kinds.DANGER : kinds.SUCCESS}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<span className={styles.clipboardIconContainer}>
|
||||
<Icon name={icons.CLIPBOARD} />
|
||||
</span>
|
||||
</span>
|
||||
</FormInputButton>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +1,93 @@
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
ComponentClass,
|
||||
FunctionComponent,
|
||||
ComponentPropsWithoutRef,
|
||||
ElementType,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import styles from './Link.css';
|
||||
|
||||
interface ReactRouterLinkProps {
|
||||
to?: string;
|
||||
}
|
||||
export type LinkProps<C extends ElementType = 'button'> =
|
||||
ComponentPropsWithoutRef<C> & {
|
||||
component?: C;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: LinkProps<C>['disabled'];
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
};
|
||||
|
||||
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
|
||||
className?: string;
|
||||
component?:
|
||||
| string
|
||||
| FunctionComponent<LinkProps>
|
||||
| ComponentClass<LinkProps, unknown>;
|
||||
to?: string;
|
||||
target?: string;
|
||||
isDisabled?: boolean;
|
||||
noRouter?: boolean;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
}
|
||||
function Link(props: LinkProps) {
|
||||
const {
|
||||
className,
|
||||
component = 'button',
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter = false,
|
||||
onPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
export default function Link<C extends ElementType = 'button'>({
|
||||
className,
|
||||
component,
|
||||
to,
|
||||
target,
|
||||
type,
|
||||
isDisabled,
|
||||
noRouter,
|
||||
onPress,
|
||||
...otherProps
|
||||
}: LinkProps<C>) {
|
||||
const Component = component || 'button';
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: SyntheticEvent) => {
|
||||
if (!isDisabled && onPress) {
|
||||
onPress(event);
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onPress?.(event);
|
||||
},
|
||||
[isDisabled, onPress]
|
||||
);
|
||||
|
||||
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
|
||||
target,
|
||||
};
|
||||
let el = component;
|
||||
|
||||
if (to) {
|
||||
if (/\w+?:\/\//.test(to)) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_blank';
|
||||
linkProps.rel = 'noreferrer';
|
||||
} else if (noRouter) {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (el === 'button' || el === 'input') {
|
||||
linkProps.type = type || 'button';
|
||||
linkProps.disabled = isDisabled;
|
||||
}
|
||||
|
||||
linkProps.className = classNames(
|
||||
const linkClass = classNames(
|
||||
className,
|
||||
styles.link,
|
||||
to && styles.to,
|
||||
isDisabled && 'isDisabled'
|
||||
);
|
||||
|
||||
const elementProps = {
|
||||
...otherProps,
|
||||
type,
|
||||
...linkProps,
|
||||
};
|
||||
if (to) {
|
||||
const toLink = /\w+?:\/\//.test(to);
|
||||
|
||||
elementProps.onClick = onClick;
|
||||
if (toLink || noRouter) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
target={target || (toLink ? '_blank' : '_self')}
|
||||
rel={toLink ? 'noreferrer' : undefined}
|
||||
className={linkClass}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement(el, elementProps);
|
||||
return (
|
||||
<RouterLink
|
||||
to={`${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`}
|
||||
target={target}
|
||||
className={linkClass}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
type={
|
||||
component === 'button' || component === 'input'
|
||||
? type || 'button'
|
||||
: type
|
||||
}
|
||||
target={target}
|
||||
className={linkClass}
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Link;
|
||||
|
||||
@@ -141,6 +141,16 @@ class SignalRConnector extends Component {
|
||||
console.error(`signalR: Unable to find handler for ${name}`);
|
||||
};
|
||||
|
||||
handleApplications = ({ action, resource }) => {
|
||||
const section = 'settings.applications';
|
||||
|
||||
if (action === 'created' || action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||
}
|
||||
};
|
||||
|
||||
handleCommand = (body) => {
|
||||
if (body.action === 'sync') {
|
||||
this.props.dispatchFetchCommands();
|
||||
@@ -150,8 +160,8 @@ class SignalRConnector extends Component {
|
||||
const resource = body.resource;
|
||||
const status = resource.status;
|
||||
|
||||
// Both sucessful and failed commands need to be
|
||||
// completed, otherwise they spin until they timeout.
|
||||
// Both successful and failed commands need to be
|
||||
// completed, otherwise they spin until they time out.
|
||||
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
this.props.dispatchFinishCommand(resource);
|
||||
@@ -160,6 +170,16 @@ class SignalRConnector extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleDownloadclient = ({ action, resource }) => {
|
||||
const section = 'settings.downloadClients';
|
||||
|
||||
if (action === 'created' || action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||
}
|
||||
};
|
||||
|
||||
handleHealth = () => {
|
||||
this.props.dispatchFetchHealth();
|
||||
};
|
||||
@@ -168,14 +188,33 @@ class SignalRConnector extends Component {
|
||||
this.props.dispatchFetchIndexerStatus();
|
||||
};
|
||||
|
||||
handleIndexer = (body) => {
|
||||
const action = body.action;
|
||||
handleIndexer = ({ action, resource }) => {
|
||||
const section = 'indexers';
|
||||
|
||||
if (action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||
if (action === 'created' || action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||
}
|
||||
};
|
||||
|
||||
handleIndexerproxy = ({ action, resource }) => {
|
||||
const section = 'settings.indexerProxies';
|
||||
|
||||
if (action === 'created' || action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||
}
|
||||
};
|
||||
|
||||
handleNotification = ({ action, resource }) => {
|
||||
const section = 'settings.notifications';
|
||||
|
||||
if (action === 'created' || action === 'updated') {
|
||||
this.props.dispatchUpdateItem({ section, ...resource });
|
||||
} else if (action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ export const PRIMARY = 'primary';
|
||||
export const PURPLE = 'purple';
|
||||
export const SUCCESS = 'success';
|
||||
export const WARNING = 'warning';
|
||||
export const QUEUE = 'queue';
|
||||
|
||||
export const all = [
|
||||
DANGER,
|
||||
@@ -19,5 +18,15 @@ export const all = [
|
||||
PURPLE,
|
||||
SUCCESS,
|
||||
WARNING,
|
||||
QUEUE
|
||||
];
|
||||
] as const;
|
||||
|
||||
export type Kind =
|
||||
| 'danger'
|
||||
| 'default'
|
||||
| 'disabled'
|
||||
| 'info'
|
||||
| 'inverse'
|
||||
| 'primary'
|
||||
| 'purple'
|
||||
| 'success'
|
||||
| 'warning';
|
||||
@@ -4,4 +4,6 @@ export const MEDIUM = 'medium';
|
||||
export const LARGE = 'large';
|
||||
export const EXTRA_LARGE = 'extraLarge';
|
||||
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const;
|
||||
|
||||
export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge';
|
||||
@@ -19,6 +19,7 @@ interface SavePayload {
|
||||
seedRatio?: number;
|
||||
seedTime?: number;
|
||||
packSeedTime?: number;
|
||||
preferMagnetUrl?: boolean;
|
||||
}
|
||||
|
||||
interface EditIndexerModalContentProps {
|
||||
@@ -65,6 +66,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
|
||||
null
|
||||
);
|
||||
const [preferMagnetUrl, setPreferMagnetUrl] = useState<
|
||||
null | string | boolean
|
||||
>(null);
|
||||
|
||||
const save = useCallback(() => {
|
||||
let hasChanges = false;
|
||||
@@ -105,6 +109,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
payload.packSeedTime = packSeedTime as number;
|
||||
}
|
||||
|
||||
if (preferMagnetUrl !== null) {
|
||||
hasChanges = true;
|
||||
payload.preferMagnetUrl = preferMagnetUrl === 'true';
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
onSavePress(payload);
|
||||
}
|
||||
@@ -118,6 +127,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
seedRatio,
|
||||
seedTime,
|
||||
packSeedTime,
|
||||
preferMagnetUrl,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
]);
|
||||
@@ -146,6 +156,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
case 'packSeedTime':
|
||||
setPackSeedTime(value);
|
||||
break;
|
||||
case 'preferMagnetUrl':
|
||||
setPreferMagnetUrl(value);
|
||||
break;
|
||||
default:
|
||||
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||
}
|
||||
@@ -254,6 +267,18 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('PreferMagnetUrl')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="preferMagnetUrl"
|
||||
value={preferMagnetUrl}
|
||||
values={enableOptions}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
.minimumSeeders,
|
||||
.seedRatio,
|
||||
.seedTime,
|
||||
.packSeedTime {
|
||||
.packSeedTime,
|
||||
.preferMagnetUrl {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 90px;
|
||||
|
||||
@@ -11,6 +11,7 @@ interface CssExports {
|
||||
'id': string;
|
||||
'minimumSeeders': string;
|
||||
'packSeedTime': string;
|
||||
'preferMagnetUrl': string;
|
||||
'priority': string;
|
||||
'privacy': string;
|
||||
'protocol': string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
@@ -74,6 +75,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')
|
||||
?.value ?? undefined;
|
||||
|
||||
const preferMagnetUrl =
|
||||
fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')
|
||||
?.value ?? undefined;
|
||||
|
||||
const rssUrl = `${window.location.origin}${
|
||||
window.Prowlarr.urlBase
|
||||
}/${id}/api?apikey=${encodeURIComponent(
|
||||
@@ -102,6 +107,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
setIsDeleteIndexerModalOpen(false);
|
||||
}, [setIsDeleteIndexerModalOpen]);
|
||||
|
||||
const checkInputCallback = useCallback(() => {
|
||||
// Mock handler to satisfy `onChange` being required for `CheckInput`.
|
||||
}, []);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
({ id, value, shiftKey }: SelectStateInputProps) => {
|
||||
selectDispatch({
|
||||
@@ -277,6 +286,21 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'preferMagnetUrl') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{preferMagnetUrl === undefined ? null : (
|
||||
<CheckInput
|
||||
name="preferMagnetUrl"
|
||||
value={preferMagnetUrl}
|
||||
isDisabled={true}
|
||||
onChange={checkInputCallback}
|
||||
/>
|
||||
)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
.minimumSeeders,
|
||||
.seedRatio,
|
||||
.seedTime,
|
||||
.packSeedTime {
|
||||
.packSeedTime,
|
||||
.preferMagnetUrl {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 90px;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface CssExports {
|
||||
'id': string;
|
||||
'minimumSeeders': string;
|
||||
'packSeedTime': string;
|
||||
'preferMagnetUrl': string;
|
||||
'priority': string;
|
||||
'privacy': string;
|
||||
'protocol': string;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { createSelector } from 'reselect';
|
||||
import Alert from 'Components/Alert';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
@@ -26,23 +24,12 @@ import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
|
||||
import Indexer, { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
||||
import useIndexer from 'Indexer/useIndexer';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import IndexerHistory from './History/IndexerHistory';
|
||||
import styles from './IndexerInfoModalContent.css';
|
||||
|
||||
function createIndexerInfoItemSelector(indexerId: number) {
|
||||
return createSelector(
|
||||
createIndexerSelectorForHook(indexerId),
|
||||
(indexer?: Indexer) => {
|
||||
return {
|
||||
indexer,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const tabs = ['details', 'categories', 'history', 'stats'];
|
||||
const TABS = ['details', 'categories', 'history', 'stats'];
|
||||
|
||||
interface IndexerInfoModalContentProps {
|
||||
indexerId: number;
|
||||
@@ -51,9 +38,7 @@ interface IndexerInfoModalContentProps {
|
||||
}
|
||||
|
||||
function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
const { indexerId, onCloneIndexerPress } = props;
|
||||
|
||||
const { indexer } = useSelector(createIndexerInfoItemSelector(indexerId));
|
||||
const { indexerId, onModalClose, onCloneIndexerPress } = props;
|
||||
|
||||
const {
|
||||
id,
|
||||
@@ -67,53 +52,53 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
protocol,
|
||||
privacy,
|
||||
capabilities = {} as IndexerCapabilities,
|
||||
} = indexer as Indexer;
|
||||
} = useIndexer(indexerId) as Indexer;
|
||||
|
||||
const { onModalClose } = props;
|
||||
|
||||
const baseUrl =
|
||||
fields.find((field) => field.name === 'baseUrl')?.value ??
|
||||
(Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
|
||||
|
||||
const vipExpiration =
|
||||
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(tabs[0]);
|
||||
const [selectedTab, setSelectedTab] = useState(TABS[0]);
|
||||
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
|
||||
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const onTabSelect = useCallback(
|
||||
(index: number) => {
|
||||
const selectedTab = tabs[index];
|
||||
const handleTabSelect = useCallback(
|
||||
(selectedIndex: number) => {
|
||||
const selectedTab = TABS[selectedIndex];
|
||||
setSelectedTab(selectedTab);
|
||||
},
|
||||
[setSelectedTab]
|
||||
);
|
||||
|
||||
const onEditIndexerPress = useCallback(() => {
|
||||
const handleEditIndexerPress = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(true);
|
||||
}, [setIsEditIndexerModalOpen]);
|
||||
|
||||
const onEditIndexerModalClose = useCallback(() => {
|
||||
const handleEditIndexerModalClose = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(false);
|
||||
}, [setIsEditIndexerModalOpen]);
|
||||
|
||||
const onDeleteIndexerPress = useCallback(() => {
|
||||
const handleDeleteIndexerPress = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(false);
|
||||
setIsDeleteIndexerModalOpen(true);
|
||||
}, [setIsDeleteIndexerModalOpen]);
|
||||
|
||||
const onDeleteIndexerModalClose = useCallback(() => {
|
||||
const handleDeleteIndexerModalClose = useCallback(() => {
|
||||
setIsDeleteIndexerModalOpen(false);
|
||||
onModalClose();
|
||||
}, [setIsDeleteIndexerModalOpen, onModalClose]);
|
||||
|
||||
const onCloneIndexerPressWrapper = useCallback(() => {
|
||||
const handleCloneIndexerPressWrapper = useCallback(() => {
|
||||
onCloneIndexerPress(id);
|
||||
onModalClose();
|
||||
}, [id, onCloneIndexerPress, onModalClose]);
|
||||
|
||||
const baseUrl =
|
||||
fields.find((field) => field.name === 'baseUrl')?.value ??
|
||||
(Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
|
||||
|
||||
const indexerUrl = baseUrl?.replace(/(:\/\/)api\./, '$1');
|
||||
|
||||
const vipExpiration =
|
||||
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{`${name}`}</ModalHeader>
|
||||
@@ -121,8 +106,8 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
<ModalBody>
|
||||
<Tabs
|
||||
className={styles.tabs}
|
||||
selectedIndex={tabs.indexOf(selectedTab)}
|
||||
onSelect={onTabSelect}
|
||||
selectedIndex={TABS.indexOf(selectedTab)}
|
||||
onSelect={handleTabSelect}
|
||||
>
|
||||
<TabList className={styles.tabList}>
|
||||
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
|
||||
@@ -178,10 +163,8 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
{translate('IndexerSite')}
|
||||
</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
{baseUrl ? (
|
||||
<Link to={baseUrl}>
|
||||
{baseUrl.replace(/(:\/\/)api\./, '$1')}
|
||||
</Link>
|
||||
{indexerUrl ? (
|
||||
<Link to={indexerUrl}>{indexerUrl}</Link>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
@@ -365,16 +348,16 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteIndexerPress}
|
||||
onPress={handleDeleteIndexerPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
<Button onPress={onCloneIndexerPressWrapper}>
|
||||
<Button onPress={handleCloneIndexerPressWrapper}>
|
||||
{translate('Clone')}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button onPress={onEditIndexerPress}>{translate('Edit')}</Button>
|
||||
<Button onPress={handleEditIndexerPress}>{translate('Edit')}</Button>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
@@ -382,14 +365,14 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
<EditIndexerModalConnector
|
||||
isOpen={isEditIndexerModalOpen}
|
||||
id={id}
|
||||
onModalClose={onEditIndexerModalClose}
|
||||
onDeleteIndexerPress={onDeleteIndexerPress}
|
||||
onModalClose={handleEditIndexerModalClose}
|
||||
onDeleteIndexerPress={handleDeleteIndexerPress}
|
||||
/>
|
||||
|
||||
<DeleteIndexerModal
|
||||
isOpen={isDeleteIndexerModalOpen}
|
||||
indexerId={id}
|
||||
onModalClose={onDeleteIndexerModalClose}
|
||||
onModalClose={handleDeleteIndexerModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
|
||||
19
frontend/src/Indexer/useIndexer.ts
Normal file
19
frontend/src/Indexer/useIndexer.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
export function createIndexerSelector(indexerId?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.indexers.itemMap,
|
||||
(state: AppState) => state.indexers.items,
|
||||
(itemMap, allIndexers) => {
|
||||
return indexerId ? allIndexers[itemMap[indexerId]] : undefined;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function useIndexer(indexerId?: number) {
|
||||
return useSelector(createIndexerSelector(indexerId));
|
||||
}
|
||||
|
||||
export default useIndexer;
|
||||
@@ -4,11 +4,13 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchApplications, fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
|
||||
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Tags from './Tags';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.tags,
|
||||
createSortedSectionSelector('tags', sortByProp('label')),
|
||||
(tags) => {
|
||||
const isFetching = tags.isFetching || tags.details.isFetching;
|
||||
const error = tags.error || tags.details.error;
|
||||
|
||||
@@ -116,6 +116,26 @@ export const sortPredicates = {
|
||||
|
||||
vipExpiration: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'vipExpiration')?.value ?? '';
|
||||
},
|
||||
|
||||
minimumSeeders: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.appMinimumSeeders')?.value ?? undefined;
|
||||
},
|
||||
|
||||
seedRatio: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.seedRatio')?.value ?? undefined;
|
||||
},
|
||||
|
||||
seedTime: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.seedTime')?.value ?? undefined;
|
||||
},
|
||||
|
||||
packSeedTime: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')?.value ?? undefined;
|
||||
},
|
||||
|
||||
preferMagnetUrl: function({ fields = [] }) {
|
||||
return fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')?.value ?? undefined;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -116,6 +116,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'preferMagnetUrl',
|
||||
label: () => translate('PreferMagnetUrl'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: () => translate('Tags'),
|
||||
|
||||
@@ -116,6 +116,7 @@ class BackupRow extends Component {
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
title={translate('RestoreBackup')}
|
||||
name={icons.RESTORE}
|
||||
onPress={this.onRestorePress}
|
||||
/>
|
||||
@@ -138,7 +139,9 @@ class BackupRow extends Component {
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteBackup')}
|
||||
message={translate('DeleteBackupMessageText', { name })}
|
||||
message={translate('DeleteBackupMessageText', {
|
||||
name
|
||||
})}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeletePress}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
|
||||
@@ -109,7 +109,7 @@ class Backups extends Component {
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadBackups')}
|
||||
{translate('BackupsLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import styles from './RestoreBackupModalContent.css';
|
||||
|
||||
function getErrorMessage(error) {
|
||||
if (!error || !error.responseJSON || !error.responseJSON.message) {
|
||||
return 'Error restoring backup';
|
||||
return translate('ErrorRestoringBackup');
|
||||
}
|
||||
|
||||
return error.responseJSON.message;
|
||||
@@ -146,7 +146,9 @@ class RestoreBackupModalContent extends Component {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
!!id && `Would you like to restore the backup '${name}'?`
|
||||
!!id && translate('WouldYouLikeToRestoreBackup', {
|
||||
name
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
@@ -203,7 +205,7 @@ class RestoreBackupModalContent extends Component {
|
||||
|
||||
<ModalFooter>
|
||||
<div className={styles.additionalInfo}>
|
||||
Note: Prowlarr will automatically restart and reload the UI during the restore process.
|
||||
{translate('RestartReloadNote')}
|
||||
</div>
|
||||
|
||||
<Button onPress={onModalClose}>
|
||||
@@ -216,7 +218,7 @@ class RestoreBackupModalContent extends Component {
|
||||
isSpinning={isRestoring}
|
||||
onPress={this.onRestorePress}
|
||||
>
|
||||
Restore
|
||||
{translate('Restore')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -84,7 +84,7 @@ function LogsTable(props) {
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
No events found
|
||||
{translate('NoEventsFound')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ function LogsTableDetailsModal(props) {
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Details
|
||||
{translate('Details')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
@@ -77,13 +77,15 @@ class LogFiles extends Component {
|
||||
<PageContentBody>
|
||||
<Alert>
|
||||
<div>
|
||||
Log files are located in: {location}
|
||||
{translate('LogFilesLocation', {
|
||||
location
|
||||
})}
|
||||
</div>
|
||||
|
||||
{
|
||||
currentLogView === 'Log Files' &&
|
||||
<div>
|
||||
The log level defaults to 'Info' and can be changed in <Link to="/settings/general">General Settings</Link>
|
||||
<InlineMarkdown data={translate('TheLogLevelDefault')} />
|
||||
</div>
|
||||
}
|
||||
</Alert>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchLogFiles } from 'Store/Actions/systemActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import combinePath from 'Utilities/String/combinePath';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import LogFiles from './LogFiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -29,7 +30,7 @@ function createMapStateToProps() {
|
||||
isFetching,
|
||||
items,
|
||||
deleteFilesExecuting,
|
||||
currentLogView: 'Log Files',
|
||||
currentLogView: translate('LogFiles'),
|
||||
location: combinePath(isWindows, appData, ['logs'])
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from 'Components/Link/Link';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './LogFilesTableRow.css';
|
||||
|
||||
class LogFilesTableRow extends Component {
|
||||
@@ -32,7 +33,7 @@ class LogFilesTableRow extends Component {
|
||||
target="_blank"
|
||||
noRouter={true}
|
||||
>
|
||||
Download
|
||||
{translate('Download')}
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -4,6 +4,7 @@ import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class LogsNavMenu extends Component {
|
||||
|
||||
@@ -50,13 +51,13 @@ class LogsNavMenu extends Component {
|
||||
<MenuItem
|
||||
to={'/system/logs/files'}
|
||||
>
|
||||
Log Files
|
||||
{translate('LogFiles')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
to={'/system/logs/files/update'}
|
||||
>
|
||||
Updater Log Files
|
||||
{translate('UpdaterLogFiles')}
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import styles from './UpdateChanges.css';
|
||||
|
||||
class UpdateChanges extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
changes
|
||||
} = this.props;
|
||||
|
||||
if (changes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uniqueChanges = [...new Set(changes)];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ul>
|
||||
{
|
||||
uniqueChanges.map((change, index) => {
|
||||
const checkChange = change.replace(/#\d{3,5}\b/g, (match, contents) => {
|
||||
return `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring(1)})`;
|
||||
});
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<InlineMarkdown data={checkChange} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UpdateChanges.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
changes: PropTypes.arrayOf(PropTypes.string)
|
||||
};
|
||||
|
||||
export default UpdateChanges;
|
||||
43
frontend/src/System/Updates/UpdateChanges.tsx
Normal file
43
frontend/src/System/Updates/UpdateChanges.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import styles from './UpdateChanges.css';
|
||||
|
||||
interface UpdateChangesProps {
|
||||
title: string;
|
||||
changes: string[];
|
||||
}
|
||||
|
||||
function UpdateChanges(props: UpdateChangesProps) {
|
||||
const { title, changes } = props;
|
||||
|
||||
if (changes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uniqueChanges = [...new Set(changes)];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ul>
|
||||
{uniqueChanges.map((change, index) => {
|
||||
const checkChange = change.replace(
|
||||
/#\d{3,5}\b/g,
|
||||
(match) =>
|
||||
`[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring(
|
||||
1
|
||||
)})`
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<InlineMarkdown data={checkChange} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpdateChanges;
|
||||
@@ -1,252 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import styles from './Updates.css';
|
||||
|
||||
class Updates extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentVersion,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError,
|
||||
items,
|
||||
isInstallingUpdate,
|
||||
updateMechanism,
|
||||
isDocker,
|
||||
updateMechanismMessage,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onInstallLatestPress
|
||||
} = this.props;
|
||||
|
||||
const hasError = !!(updatesError || generalSettingsError);
|
||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !items.length;
|
||||
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
const externalUpdaterPrefix = 'Unable to update Prowlarr directly,';
|
||||
const externalUpdaterMessages = {
|
||||
external: 'Prowlarr is configured to use an external update mechanism',
|
||||
apt: 'use apt to install the update',
|
||||
docker: 'update the docker container to receive the update'
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Updates')}>
|
||||
<PageContentBody>
|
||||
{
|
||||
!isPopulated && !hasError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
noUpdates &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoUpdatesAreAvailable')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdateToInstall &&
|
||||
<div className={styles.messageContainer}>
|
||||
{
|
||||
(updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ?
|
||||
<SpinnerButton
|
||||
className={styles.updateAvailable}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingUpdate}
|
||||
onPress={onInstallLatestPress}
|
||||
>
|
||||
Install Latest
|
||||
</SpinnerButton> :
|
||||
|
||||
<Fragment>
|
||||
<Icon
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
size={30}
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
{externalUpdaterPrefix} <InlineMarkdown data={updateMechanismMessage || externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} />
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
noUpdateToInstall &&
|
||||
<div className={styles.messageContainer}>
|
||||
<Icon
|
||||
className={styles.upToDateIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={30}
|
||||
/>
|
||||
|
||||
<div className={styles.message}>
|
||||
{translate('TheLatestVersionIsAlreadyInstalled')}
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdates &&
|
||||
<div>
|
||||
{
|
||||
items.map((update) => {
|
||||
const hasChanges = !!update.changes;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={update.version}
|
||||
className={styles.update}
|
||||
>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.version}>{update.version}</div>
|
||||
<div className={styles.space}>—</div>
|
||||
<div
|
||||
className={styles.date}
|
||||
title={formatDateTime(update.releaseDate, longDateFormat, timeFormat)}
|
||||
>
|
||||
{formatDate(update.releaseDate, shortDateFormat)}
|
||||
</div>
|
||||
|
||||
{
|
||||
update.branch === 'master' ?
|
||||
null:
|
||||
<Label
|
||||
className={styles.label}
|
||||
>
|
||||
{update.branch}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
update.version === currentVersion ?
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.SUCCESS}
|
||||
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||
>
|
||||
Currently Installed
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
update.version !== currentVersion && update.installedOn ?
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.INVERSE}
|
||||
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
|
||||
>
|
||||
Previously Installed
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!hasChanges &&
|
||||
<div>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasChanges &&
|
||||
<div className={styles.changes}>
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!updatesError &&
|
||||
<div>
|
||||
Failed to fetch updates
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!generalSettingsError &&
|
||||
<div>
|
||||
Failed to update settings
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Updates.propTypes = {
|
||||
currentVersion: PropTypes.string.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
updatesError: PropTypes.object,
|
||||
generalSettingsError: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
isInstallingUpdate: PropTypes.bool.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
updateMechanism: PropTypes.string,
|
||||
updateMechanismMessage: PropTypes.string,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onInstallLatestPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Updates;
|
||||
303
frontend/src/System/Updates/Updates.tsx
Normal file
303
frontend/src/System/Updates/Updates.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import { UpdateMechanism } from 'typings/Settings/General';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import styles from './Updates.css';
|
||||
|
||||
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
|
||||
|
||||
function createUpdatesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.system.updates,
|
||||
(state: AppState) => state.settings.general,
|
||||
(updates, generalSettings) => {
|
||||
const { error: updatesError, items } = updates;
|
||||
|
||||
const isFetching = updates.isFetching || generalSettings.isFetching;
|
||||
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError: generalSettings.error,
|
||||
items,
|
||||
updateMechanism: generalSettings.item.updateMechanism,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function Updates() {
|
||||
const currentVersion = useSelector((state: AppState) => state.app.version);
|
||||
const { packageUpdateMechanismMessage } = useSelector(
|
||||
createSystemStatusSelector()
|
||||
);
|
||||
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
const isInstallingUpdate = useSelector(
|
||||
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
|
||||
);
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError,
|
||||
items,
|
||||
updateMechanism,
|
||||
} = useSelector(createUpdatesSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
|
||||
const hasError = !!(updatesError || generalSettingsError);
|
||||
const hasUpdates = isPopulated && !hasError && items.length > 0;
|
||||
const noUpdates = isPopulated && !hasError && !items.length;
|
||||
|
||||
const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError');
|
||||
const externalUpdaterMessages: Partial<Record<UpdateMechanism, string>> = {
|
||||
external: translate('ExternalUpdater'),
|
||||
apt: translate('AptUpdater'),
|
||||
docker: translate('DockerUpdater'),
|
||||
};
|
||||
|
||||
const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => {
|
||||
const majorVersion = parseInt(
|
||||
currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
const latestVersion = items[0]?.version;
|
||||
const latestMajorVersion = parseInt(
|
||||
latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
|
||||
);
|
||||
|
||||
return {
|
||||
isMajorUpdate: latestMajorVersion > majorVersion,
|
||||
hasUpdateToInstall: items.some(
|
||||
(update) => update.installable && update.latest
|
||||
),
|
||||
};
|
||||
}, [currentVersion, items]);
|
||||
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
const handleInstallLatestPress = useCallback(() => {
|
||||
if (isMajorUpdate) {
|
||||
setIsMajorUpdateModalOpen(true);
|
||||
} else {
|
||||
dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
|
||||
}
|
||||
}, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
|
||||
|
||||
const handleInstallLatestMajorVersionPress = useCallback(() => {
|
||||
setIsMajorUpdateModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.APPLICATION_UPDATE,
|
||||
installMajorUpdate: true,
|
||||
})
|
||||
);
|
||||
}, [setIsMajorUpdateModalOpen, dispatch]);
|
||||
|
||||
const handleCancelMajorVersionPress = useCallback(() => {
|
||||
setIsMajorUpdateModalOpen(false);
|
||||
}, [setIsMajorUpdateModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUpdates());
|
||||
dispatch(fetchGeneralSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Updates')}>
|
||||
<PageContentBody>
|
||||
{isPopulated || hasError ? null : <LoadingIndicator />}
|
||||
|
||||
{noUpdates ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoUpdatesAreAvailable')}</Alert>
|
||||
) : null}
|
||||
|
||||
{hasUpdateToInstall ? (
|
||||
<div className={styles.messageContainer}>
|
||||
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingUpdate}
|
||||
onPress={handleInstallLatestPress}
|
||||
>
|
||||
{translate('InstallLatest')}
|
||||
</SpinnerButton>
|
||||
) : (
|
||||
<>
|
||||
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
|
||||
|
||||
<div className={styles.message}>
|
||||
{externalUpdaterPrefix}{' '}
|
||||
<InlineMarkdown
|
||||
data={
|
||||
packageUpdateMechanismMessage ||
|
||||
externalUpdaterMessages[updateMechanism] ||
|
||||
externalUpdaterMessages.external
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isFetching ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{noUpdateToInstall && (
|
||||
<div className={styles.messageContainer}>
|
||||
<Icon
|
||||
className={styles.upToDateIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={30}
|
||||
/>
|
||||
<div className={styles.message}>{translate('OnLatestVersion')}</div>
|
||||
|
||||
{isFetching && (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasUpdates && (
|
||||
<div>
|
||||
{items.map((update) => {
|
||||
return (
|
||||
<div key={update.version} className={styles.update}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.version}>{update.version}</div>
|
||||
<div className={styles.space}>—</div>
|
||||
<div
|
||||
className={styles.date}
|
||||
title={formatDateTime(
|
||||
update.releaseDate,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{formatDate(update.releaseDate, shortDateFormat)}
|
||||
</div>
|
||||
|
||||
{update.branch === 'master' ? null : (
|
||||
<Label className={styles.label}>{update.branch}</Label>
|
||||
)}
|
||||
|
||||
{update.version === currentVersion ? (
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.SUCCESS}
|
||||
title={formatDateTime(
|
||||
update.installedOn,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{translate('CurrentlyInstalled')}
|
||||
</Label>
|
||||
) : null}
|
||||
|
||||
{update.version !== currentVersion && update.installedOn ? (
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kinds.INVERSE}
|
||||
title={formatDateTime(
|
||||
update.installedOn,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
>
|
||||
{translate('PreviouslyInstalled')}
|
||||
</Label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{update.changes ? (
|
||||
<div>
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>{translate('MaintenanceRelease')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatesError ? (
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('FailedToFetchUpdates')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{generalSettingsError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('FailedToFetchSettings')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMajorUpdateModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('InstallMajorVersionUpdate')}
|
||||
message={
|
||||
<div>
|
||||
<div>{translate('InstallMajorVersionUpdateMessage')}</div>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('InstallMajorVersionUpdateMessageLink', {
|
||||
domain: 'prowlarr.com',
|
||||
url: 'https://prowlarr.com/#downloads',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Install')}
|
||||
onConfirm={handleInstallLatestMajorVersionPress}
|
||||
onCancel={handleCancelMajorVersionPress}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Updates;
|
||||
@@ -1,101 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import Updates from './Updates';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app.version,
|
||||
createSystemStatusSelector(),
|
||||
(state) => state.system.updates,
|
||||
(state) => state.settings.general,
|
||||
createUISettingsSelector(),
|
||||
createSystemStatusSelector(),
|
||||
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE),
|
||||
(
|
||||
currentVersion,
|
||||
status,
|
||||
updates,
|
||||
generalSettings,
|
||||
uiSettings,
|
||||
systemStatus,
|
||||
isInstallingUpdate
|
||||
) => {
|
||||
const {
|
||||
error: updatesError,
|
||||
items
|
||||
} = updates;
|
||||
|
||||
const isFetching = updates.isFetching || generalSettings.isFetching;
|
||||
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
|
||||
|
||||
return {
|
||||
currentVersion,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
updatesError,
|
||||
generalSettingsError: generalSettings.error,
|
||||
items,
|
||||
isInstallingUpdate,
|
||||
isDocker: systemStatus.isDocker,
|
||||
updateMechanism: generalSettings.item.updateMechanism,
|
||||
updateMechanismMessage: status.packageUpdateMechanismMessage,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchUpdates: fetchUpdates,
|
||||
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class UpdatesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchUpdates();
|
||||
this.props.dispatchFetchGeneralSettings();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInstallLatestPress = () => {
|
||||
this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Updates
|
||||
onInstallLatestPress={this.onInstallLatestPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UpdatesConnector.propTypes = {
|
||||
dispatchFetchUpdates: PropTypes.func.isRequired,
|
||||
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector);
|
||||
@@ -1,7 +1,9 @@
|
||||
let i = 0;
|
||||
|
||||
// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022)
|
||||
|
||||
/**
|
||||
* @deprecated Use React's useId() instead
|
||||
* @returns An HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022)
|
||||
*/
|
||||
export default function getUniqueElementId() {
|
||||
return `id-${i++}`;
|
||||
}
|
||||
|
||||
45
frontend/src/typings/Settings/General.ts
Normal file
45
frontend/src/typings/Settings/General.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export type UpdateMechanism =
|
||||
| 'builtIn'
|
||||
| 'script'
|
||||
| 'external'
|
||||
| 'apt'
|
||||
| 'docker';
|
||||
|
||||
export default interface General {
|
||||
bindAddress: string;
|
||||
port: number;
|
||||
sslPort: number;
|
||||
enableSsl: boolean;
|
||||
launchBrowser: boolean;
|
||||
authenticationMethod: string;
|
||||
authenticationRequired: string;
|
||||
analyticsEnabled: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
logLevel: string;
|
||||
consoleLogLevel: string;
|
||||
branch: string;
|
||||
apiKey: string;
|
||||
sslCertPath: string;
|
||||
sslCertPassword: string;
|
||||
urlBase: string;
|
||||
instanceName: string;
|
||||
applicationUrl: string;
|
||||
updateAutomatically: boolean;
|
||||
updateMechanism: UpdateMechanism;
|
||||
updateScriptPath: string;
|
||||
proxyEnabled: boolean;
|
||||
proxyType: string;
|
||||
proxyHostname: string;
|
||||
proxyPort: number;
|
||||
proxyUsername: string;
|
||||
proxyPassword: string;
|
||||
proxyBypassFilter: string;
|
||||
proxyBypassLocalAddresses: boolean;
|
||||
certificateValidation: string;
|
||||
backupFolder: string;
|
||||
backupInterval: number;
|
||||
backupRetention: number;
|
||||
id: number;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface UiSettings {
|
||||
export default interface UiSettings {
|
||||
theme: 'auto' | 'dark' | 'light';
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
@@ -22,6 +22,7 @@ interface SystemStatus {
|
||||
osVersion: string;
|
||||
packageAuthor: string;
|
||||
packageUpdateMechanism: string;
|
||||
packageUpdateMechanismMessage: string;
|
||||
packageVersion: string;
|
||||
runtimeName: string;
|
||||
runtimeVersion: string;
|
||||
|
||||
78
package.json
78
package.json
@@ -30,28 +30,27 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.100.0",
|
||||
"@sentry/integrations": "7.100.0",
|
||||
"@types/node": "18.19.31",
|
||||
"@sentry/browser": "7.119.1",
|
||||
"@sentry/integrations": "7.119.1",
|
||||
"@types/node": "20.16.11",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"chart.js": "4.4.3",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
"chart.js": "4.4.4",
|
||||
"classnames": "2.5.1",
|
||||
"connected-react-router": "6.9.3",
|
||||
"copy-to-clipboard": "3.3.3",
|
||||
"element-class": "0.2.2",
|
||||
"filesize": "10.0.7",
|
||||
"filesize": "10.1.6",
|
||||
"history": "4.10.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.7.1",
|
||||
"lodash": "4.17.21",
|
||||
"mobile-detect": "1.4.5",
|
||||
"moment": "2.29.4",
|
||||
"moment": "2.30.1",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.11.1",
|
||||
"qs": "6.13.0",
|
||||
"react": "17.0.2",
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
@@ -65,7 +64,6 @@
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "2.9.4",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.2.0",
|
||||
"react-measure": "1.4.7",
|
||||
"react-popper": "1.3.7",
|
||||
"react-redux": "7.2.4",
|
||||
@@ -75,55 +73,55 @@
|
||||
"react-text-truncate": "0.19.0",
|
||||
"react-use-measure": "2.1.1",
|
||||
"react-virtualized": "9.21.1",
|
||||
"react-window": "1.8.8",
|
||||
"react-window": "1.8.10",
|
||||
"redux": "4.2.1",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.4.2",
|
||||
"reselect": "4.1.7",
|
||||
"reselect": "4.1.8",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.2",
|
||||
"@babel/eslint-parser": "7.25.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.7",
|
||||
"@babel/core": "7.25.8",
|
||||
"@babel/eslint-parser": "7.25.8",
|
||||
"@babel/plugin-proposal-export-default-from": "7.25.8",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.25.3",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@types/lodash": "4.14.194",
|
||||
"@types/react-document-title": "2.0.9",
|
||||
"@babel/preset-env": "7.25.8",
|
||||
"@babel/preset-react": "7.25.7",
|
||||
"@babel/preset-typescript": "7.25.7",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/react-document-title": "2.0.10",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-text-truncate": "0.14.1",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@types/webpack-livereload-plugin": "2.3.3",
|
||||
"@types/react-text-truncate": "0.19.0",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/webpack-livereload-plugin": "2.3.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"are-you-es5": "2.1.2",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.38.0",
|
||||
"core-js": "3.38.1",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "8.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||
"eslint-plugin-react": "7.37.1",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"html-webpack-plugin": "5.6.0",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.41",
|
||||
"mini-css-extract-plugin": "2.9.1",
|
||||
"postcss": "8.4.47",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
@@ -132,17 +130,15 @@
|
||||
"postcss-url": "10.1.3",
|
||||
"prettier": "2.8.8",
|
||||
"require-nocache": "1.0.0",
|
||||
"rimraf": "4.4.1",
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"rimraf": "6.0.1",
|
||||
"style-loader": "3.3.2",
|
||||
"stylelint": "15.6.1",
|
||||
"stylelint-order": "6.0.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-loader": "9.4.2",
|
||||
"stylelint-order": "6.0.4",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-loader": "9.5.1",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.94.0",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-livereload-plugin": "3.0.2"
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"https://beyond-hd.me/torrent/download/the-next-365-days-2022-2160p-nf-web-dl-dual-ddp-51-dovi-hdr-hevc-apex.225146.2b51db35e1912ffc138825a12b9933d2")]
|
||||
[TestCase(@"https://anthelion.me/api.php?api_key=2b51db35e1910123321025a12b9933d2&o=json&t=movie&q=&tmdb=&imdb=&cat=&limit=100&offset=0")]
|
||||
[TestCase(@"https://avistaz.to/api/v1/jackett/auth: username=mySecret&password=mySecret&pid=mySecret")]
|
||||
[TestCase(@"https://www.sharewood.tv/api/2b51db35e1910123321025a12b9933d2/last-torrents")]
|
||||
|
||||
// Indexer and Download Client Responses
|
||||
|
||||
|
||||
@@ -133,11 +133,16 @@ namespace NzbDrone.Common.Test
|
||||
|
||||
[TestCase(@"C:\test\", @"C:\Test\mydir")]
|
||||
[TestCase(@"C:\test", @"C:\Test\mydir\")]
|
||||
public void path_should_be_parent_on_windows_only(string parentPath, string childPath)
|
||||
public void windows_path_should_be_parent(string parentPath, string childPath)
|
||||
{
|
||||
var expectedResult = OsInfo.IsWindows;
|
||||
parentPath.IsParentPath(childPath).Should().Be(true);
|
||||
}
|
||||
|
||||
parentPath.IsParentPath(childPath).Should().Be(expectedResult);
|
||||
[TestCase("/test", "/test/mydir/")]
|
||||
[TestCase("/test/", "/test/mydir")]
|
||||
public void posix_path_should_be_parent(string parentPath, string childPath)
|
||||
{
|
||||
parentPath.IsParentPath(childPath).Should().Be(true);
|
||||
}
|
||||
|
||||
[TestCase(@"C:\Test\mydir", @"C:\Test")]
|
||||
@@ -145,20 +150,57 @@ namespace NzbDrone.Common.Test
|
||||
[TestCase(@"C:\", null)]
|
||||
[TestCase(@"\\server\share", null)]
|
||||
[TestCase(@"\\server\share\test", @"\\server\share")]
|
||||
public void path_should_return_parent_windows(string path, string parentPath)
|
||||
public void windows_path_should_return_parent(string path, string parentPath)
|
||||
{
|
||||
WindowsOnly();
|
||||
path.GetParentPath().Should().Be(parentPath);
|
||||
}
|
||||
|
||||
[TestCase(@"/", null)]
|
||||
[TestCase(@"/test", "/")]
|
||||
public void path_should_return_parent_mono(string path, string parentPath)
|
||||
[TestCase(@"/test/tv", "/test")]
|
||||
public void unix_path_should_return_parent(string path, string parentPath)
|
||||
{
|
||||
PosixOnly();
|
||||
path.GetParentPath().Should().Be(parentPath);
|
||||
}
|
||||
|
||||
[TestCase(@"C:\Test\mydir", "Test")]
|
||||
[TestCase(@"C:\Test\", @"C:\")]
|
||||
[TestCase(@"C:\Test", @"C:\")]
|
||||
[TestCase(@"C:\", null)]
|
||||
[TestCase(@"\\server\share", null)]
|
||||
[TestCase(@"\\server\share\test", @"\\server\share")]
|
||||
public void path_should_return_parent_name_windows(string path, string parentPath)
|
||||
{
|
||||
path.GetParentName().Should().Be(parentPath);
|
||||
}
|
||||
|
||||
[TestCase(@"/", null)]
|
||||
[TestCase(@"/test", "/")]
|
||||
[TestCase(@"/test/tv", "test")]
|
||||
public void path_should_return_parent_name_mono(string path, string parentPath)
|
||||
{
|
||||
path.GetParentName().Should().Be(parentPath);
|
||||
}
|
||||
|
||||
[TestCase(@"C:\Test\mydir", "mydir")]
|
||||
[TestCase(@"C:\Test\", "Test")]
|
||||
[TestCase(@"C:\Test", "Test")]
|
||||
[TestCase(@"C:\", "C:\\")]
|
||||
[TestCase(@"\\server\share", @"\\server\share")]
|
||||
[TestCase(@"\\server\share\test", "test")]
|
||||
public void path_should_return_directory_name_windows(string path, string parentPath)
|
||||
{
|
||||
path.GetDirectoryName().Should().Be(parentPath);
|
||||
}
|
||||
|
||||
[TestCase(@"/", "/")]
|
||||
[TestCase(@"/test", "test")]
|
||||
[TestCase(@"/test/tv", "tv")]
|
||||
public void path_should_return_directory_name_mono(string path, string parentPath)
|
||||
{
|
||||
path.GetDirectoryName().Should().Be(parentPath);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void path_should_return_parent_for_oversized_path()
|
||||
{
|
||||
@@ -166,7 +208,7 @@ namespace NzbDrone.Common.Test
|
||||
|
||||
// This test will fail on Windows if long path support is not enabled: https://www.howtogeek.com/266621/how-to-make-windows-10-accept-file-paths-over-260-characters/
|
||||
// It will also fail if the app isn't configured to use long path (such as resharper): https://blogs.msdn.microsoft.com/jeremykuhne/2016/07/30/net-4-6-2-and-long-paths-on-windows-10/
|
||||
var path = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories".AsOsAgnostic();
|
||||
var path = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories".AsOsAgnostic();
|
||||
var parentPath = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing".AsOsAgnostic();
|
||||
|
||||
path.GetParentPath().Should().Be(parentPath);
|
||||
@@ -350,5 +392,46 @@ namespace NzbDrone.Common.Test
|
||||
PosixOnly();
|
||||
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase(@"C:\", @"C:\")]
|
||||
[TestCase(@"C:\\", @"C:\")]
|
||||
[TestCase(@"C:\Test", @"C:\Test")]
|
||||
[TestCase(@"C:\Test\", @"C:\Test")]
|
||||
[TestCase(@"\\server\share", @"\\server\share")]
|
||||
[TestCase(@"\\server\share\", @"\\server\share")]
|
||||
public void windows_path_should_return_clean_path(string path, string cleanPath)
|
||||
{
|
||||
path.GetCleanPath().Should().Be(cleanPath);
|
||||
}
|
||||
|
||||
[TestCase("/", "/")]
|
||||
[TestCase("//", "/")]
|
||||
[TestCase("/test", "/test")]
|
||||
[TestCase("/test/", "/test")]
|
||||
[TestCase("/test//", "/test")]
|
||||
public void unix_path_should_return_clean_path(string path, string cleanPath)
|
||||
{
|
||||
path.GetCleanPath().Should().Be(cleanPath);
|
||||
}
|
||||
|
||||
[TestCase(@"C:\Test\", @"C:\Test\Series Title", "Series Title")]
|
||||
[TestCase(@"C:\Test\", @"C:\Test\Collection\Series Title", @"Collection\Series Title")]
|
||||
[TestCase(@"C:\Test\mydir\", @"C:\Test\mydir\Collection\Series Title", @"Collection\Series Title")]
|
||||
[TestCase(@"\\server\share", @"\\server\share\Series Title", "Series Title")]
|
||||
[TestCase(@"\\server\share\mydir\", @"\\server\share\mydir\/Collection\Series Title", @"Collection\Series Title")]
|
||||
public void windows_path_should_return_relative_path(string parentPath, string childPath, string relativePath)
|
||||
{
|
||||
parentPath.GetRelativePath(childPath).Should().Be(relativePath);
|
||||
}
|
||||
|
||||
[TestCase(@"/test", "/test/Series Title", "Series Title")]
|
||||
[TestCase(@"/test/", "/test/Collection/Series Title", "Collection/Series Title")]
|
||||
[TestCase(@"/test/mydir", "/test/mydir/Series Title", "Series Title")]
|
||||
[TestCase(@"/test/mydir/", "/test/mydir/Collection/Series Title", "Collection/Series Title")]
|
||||
[TestCase(@"/test/mydir/", @"/test/mydir/\Collection/Series Title", "Collection/Series Title")]
|
||||
public void unix_path_should_return_relative_path(string parentPath, string childPath, string relativePath)
|
||||
{
|
||||
parentPath.GetRelativePath(childPath).Should().Be(relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Common.Disk
|
||||
@@ -9,6 +10,8 @@ namespace NzbDrone.Common.Disk
|
||||
private readonly string _path;
|
||||
private readonly OsPathKind _kind;
|
||||
|
||||
private static readonly Regex UncPathRegex = new Regex(@"(?<unc>^\\\\(?:\?\\UNC\\)?[^\\]+\\[^\\]+)(?:\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public OsPath(string path)
|
||||
{
|
||||
if (path == null)
|
||||
@@ -96,6 +99,29 @@ namespace NzbDrone.Common.Disk
|
||||
return path;
|
||||
}
|
||||
|
||||
private static string TrimTrailingSlash(string path, OsPathKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case OsPathKind.Windows when !path.EndsWith(":\\"):
|
||||
while (!path.EndsWith(":\\") && path.EndsWith('\\'))
|
||||
{
|
||||
path = path[..^1];
|
||||
}
|
||||
|
||||
return path;
|
||||
case OsPathKind.Unix when path != "/":
|
||||
while (path != "/" && path.EndsWith('/'))
|
||||
{
|
||||
path = path[..^1];
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public OsPathKind Kind => _kind;
|
||||
|
||||
public bool IsWindowsPath => _kind == OsPathKind.Windows;
|
||||
@@ -130,7 +156,19 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
return new OsPath(null);
|
||||
return Null;
|
||||
}
|
||||
|
||||
var rootLength = GetRootLength();
|
||||
|
||||
if (rootLength == _path.Length)
|
||||
{
|
||||
return Null;
|
||||
}
|
||||
|
||||
if (rootLength > index + 1)
|
||||
{
|
||||
return new OsPath(_path.Substring(0, rootLength));
|
||||
}
|
||||
|
||||
return new OsPath(_path.Substring(0, index), _kind).AsDirectory();
|
||||
@@ -139,6 +177,8 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
public string FullPath => _path;
|
||||
|
||||
public string PathWithoutTrailingSlash => TrimTrailingSlash(_path, _kind);
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get
|
||||
@@ -161,6 +201,29 @@ namespace NzbDrone.Common.Disk
|
||||
}
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
// Meant to behave similar to DirectoryInfo.Name
|
||||
get
|
||||
{
|
||||
var index = GetFileNameIndex();
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
return PathWithoutTrailingSlash;
|
||||
}
|
||||
|
||||
var rootLength = GetRootLength();
|
||||
|
||||
if (rootLength > index + 1)
|
||||
{
|
||||
return _path.Substring(0, rootLength);
|
||||
}
|
||||
|
||||
return TrimTrailingSlash(_path.Substring(index).TrimStart('/', '\\'), _kind);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValid => _path.IsPathValid(PathValidationType.CurrentOs);
|
||||
|
||||
private int GetFileNameIndex()
|
||||
@@ -190,11 +253,50 @@ namespace NzbDrone.Common.Disk
|
||||
return index;
|
||||
}
|
||||
|
||||
private int GetRootLength()
|
||||
{
|
||||
if (!IsRooted)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_kind == OsPathKind.Unix)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (_kind == OsPathKind.Windows)
|
||||
{
|
||||
if (HasWindowsDriveLetter(_path))
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
var uncMatch = UncPathRegex.Match(_path);
|
||||
|
||||
// \\?\UNC\server\share\ or \\server\share
|
||||
if (uncMatch.Success)
|
||||
{
|
||||
return uncMatch.Groups["unc"].Length;
|
||||
}
|
||||
|
||||
// \\?\C:\
|
||||
if (_path.StartsWith(@"\\?\"))
|
||||
{
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private string[] GetFragments()
|
||||
{
|
||||
return _path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public static OsPath Null => new (null);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _path;
|
||||
@@ -267,6 +369,11 @@ namespace NzbDrone.Common.Disk
|
||||
}
|
||||
|
||||
public bool Equals(OsPath other)
|
||||
{
|
||||
return Equals(other, false);
|
||||
}
|
||||
|
||||
public bool Equals(OsPath other, bool ignoreTrailingSlash)
|
||||
{
|
||||
if (ReferenceEquals(other, null))
|
||||
{
|
||||
@@ -278,8 +385,8 @@ namespace NzbDrone.Common.Disk
|
||||
return true;
|
||||
}
|
||||
|
||||
var left = _path;
|
||||
var right = other._path;
|
||||
var left = ignoreTrailingSlash ? PathWithoutTrailingSlash : _path;
|
||||
var right = ignoreTrailingSlash ? other.PathWithoutTrailingSlash : other._path;
|
||||
|
||||
if (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
@@ -25,22 +24,25 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
|
||||
static OsInfo()
|
||||
{
|
||||
var platform = Environment.OSVersion.Platform;
|
||||
|
||||
switch (platform)
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
case PlatformID.Win32NT:
|
||||
{
|
||||
Os = Os.Windows;
|
||||
break;
|
||||
}
|
||||
|
||||
case PlatformID.MacOSX:
|
||||
case PlatformID.Unix:
|
||||
{
|
||||
Os = GetPosixFlavour();
|
||||
break;
|
||||
}
|
||||
Os = Os.Windows;
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Os = Os.Osx;
|
||||
}
|
||||
else if (OperatingSystem.IsFreeBSD())
|
||||
{
|
||||
Os = Os.Bsd;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if ISMUSL
|
||||
Os = Os.LinuxMusl;
|
||||
#else
|
||||
Os = Os.Linux;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,59 +86,6 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
IsDocker = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static Os GetPosixFlavour()
|
||||
{
|
||||
var output = RunAndCapture("uname", "-s");
|
||||
|
||||
if (output.StartsWith("Darwin"))
|
||||
{
|
||||
return Os.Osx;
|
||||
}
|
||||
else if (output.Contains("BSD"))
|
||||
{
|
||||
return Os.Bsd;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if ISMUSL
|
||||
return Os.LinuxMusl;
|
||||
#else
|
||||
return Os.Linux;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static string RunAndCapture(string filename, string args)
|
||||
{
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = filename,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true
|
||||
};
|
||||
|
||||
var output = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
using (var p = Process.Start(processStartInfo))
|
||||
{
|
||||
// To avoid deadlocks, always read the output stream first and then wait.
|
||||
output = p.StandardOutput.ReadToEnd();
|
||||
|
||||
p.WaitForExit(1000);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
output = string.Empty;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IOsInfo
|
||||
|
||||
@@ -25,8 +25,6 @@ namespace NzbDrone.Common.Extensions
|
||||
private static readonly string UPDATE_CLIENT_FOLDER_NAME = "Prowlarr.Update" + Path.DirectorySeparatorChar;
|
||||
private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar;
|
||||
|
||||
private static readonly Regex PARENT_PATH_END_SLASH_REGEX = new Regex(@"(?<!:)\\$", RegexOptions.Compiled);
|
||||
|
||||
public static string CleanFilePath(this string path)
|
||||
{
|
||||
if (path.IsNotNullOrWhiteSpace())
|
||||
@@ -87,55 +85,50 @@ namespace NzbDrone.Common.Extensions
|
||||
throw new NotParentException("{0} is not a child of {1}", childPath, parentPath);
|
||||
}
|
||||
|
||||
return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar);
|
||||
return childPath.Substring(parentPath.Length).Trim('\\', '/');
|
||||
}
|
||||
|
||||
public static string GetParentPath(this string childPath)
|
||||
{
|
||||
var cleanPath = OsInfo.IsWindows
|
||||
? PARENT_PATH_END_SLASH_REGEX.Replace(childPath, "")
|
||||
: childPath.TrimEnd(Path.DirectorySeparatorChar);
|
||||
var path = new OsPath(childPath).Directory;
|
||||
|
||||
if (cleanPath.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return path == OsPath.Null ? null : path.PathWithoutTrailingSlash;
|
||||
}
|
||||
|
||||
return Directory.GetParent(cleanPath)?.FullName;
|
||||
public static string GetParentName(this string childPath)
|
||||
{
|
||||
var path = new OsPath(childPath).Directory;
|
||||
|
||||
return path == OsPath.Null ? null : path.Name;
|
||||
}
|
||||
|
||||
public static string GetDirectoryName(this string childPath)
|
||||
{
|
||||
var path = new OsPath(childPath);
|
||||
|
||||
return path == OsPath.Null ? null : path.Name;
|
||||
}
|
||||
|
||||
public static string GetCleanPath(this string path)
|
||||
{
|
||||
var cleanPath = OsInfo.IsWindows
|
||||
? PARENT_PATH_END_SLASH_REGEX.Replace(path, "")
|
||||
: path.TrimEnd(Path.DirectorySeparatorChar);
|
||||
var osPath = new OsPath(path);
|
||||
|
||||
return cleanPath;
|
||||
return osPath == OsPath.Null ? null : osPath.PathWithoutTrailingSlash;
|
||||
}
|
||||
|
||||
public static bool IsParentPath(this string parentPath, string childPath)
|
||||
{
|
||||
if (parentPath != "/" && !parentPath.EndsWith(":\\"))
|
||||
{
|
||||
parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar);
|
||||
}
|
||||
var parent = new OsPath(parentPath);
|
||||
var child = new OsPath(childPath);
|
||||
|
||||
if (childPath != "/" && !parentPath.EndsWith(":\\"))
|
||||
while (child.Directory != OsPath.Null)
|
||||
{
|
||||
childPath = childPath.TrimEnd(Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
var parent = new DirectoryInfo(parentPath);
|
||||
var child = new DirectoryInfo(childPath);
|
||||
|
||||
while (child.Parent != null)
|
||||
{
|
||||
if (child.Parent.FullName.Equals(parent.FullName, DiskProviderBase.PathStringComparison))
|
||||
if (child.Directory.Equals(parent, true))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
child = child.Parent;
|
||||
child = child.Directory;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -150,14 +143,14 @@ namespace NzbDrone.Common.Extensions
|
||||
return false;
|
||||
}
|
||||
|
||||
if (path.Trim() != path)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only check for leading or trailing spaces for path when running on Windows.
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
if (path.Trim() != path)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var directoryInfo = new DirectoryInfo(path);
|
||||
|
||||
while (directoryInfo != null)
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
if (response.HasHttpRedirect && !RuntimeInfo.IsProduction)
|
||||
{
|
||||
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
|
||||
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.RedirectUrl);
|
||||
}
|
||||
|
||||
if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)))
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new (@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?<secret>[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"(?:sharewood)\.[a-z]{2,3}/api/(?<secret>[a-z0-9]{16,})/", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// UNIT3D
|
||||
new (@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<PackageReference Include="NLog" Version="5.3.3" />
|
||||
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.12" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.7" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.8" />
|
||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 19:20:19"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
|
||||
torrentInfo.Size.Should().Be(8300512414);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -35,18 +35,18 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
{
|
||||
_updatePackage = new UpdatePackage
|
||||
{
|
||||
FileName = "NzbDrone.develop.2.0.0.0.tar.gz",
|
||||
FileName = "NzbDrone.develop.1.0.0.0.tar.gz",
|
||||
Url = "http://download.sonarr.tv/v2/develop/mono/NzbDrone.develop.tar.gz",
|
||||
Version = new Version("2.0.0.0")
|
||||
Version = new Version("1.0.0.0")
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_updatePackage = new UpdatePackage
|
||||
{
|
||||
FileName = "NzbDrone.develop.2.0.0.0.zip",
|
||||
FileName = "NzbDrone.develop.1.0.0.0.zip",
|
||||
Url = "http://download.sonarr.tv/v2/develop/windows/NzbDrone.develop.zip",
|
||||
Version = new Version("2.0.0.0")
|
||||
Version = new Version("1.0.0.0")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,17 +90,6 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_update_if_inside_docker()
|
||||
{
|
||||
Mocker.GetMock<IOsInfo>().Setup(x => x.IsDocker).Returns(true);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
|
||||
Mocker.GetMock<IProcessProvider>()
|
||||
.Verify(c => c.Start(It.IsAny<string>(), It.Is<string>(s => s.StartsWith("12")), null, null, null), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_sandbox_before_update_if_folder_exists()
|
||||
{
|
||||
@@ -338,6 +327,28 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
.Verify(v => v.SaveConfigDictionary(It.Is<Dictionary<string, object>>(d => d.ContainsKey("Branch") && (string)d["Branch"] == "fake")), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_update_with_built_in_updater_inside_docker_container()
|
||||
{
|
||||
Mocker.GetMock<IDeploymentInfoProvider>().Setup(x => x.PackageUpdateMechanism).Returns(UpdateMechanism.Docker);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
|
||||
Mocker.GetMock<IProcessProvider>()
|
||||
.Verify(c => c.Start(It.IsAny<string>(), It.Is<string>(s => s.StartsWith("12")), null, null, null), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_update_with_built_in_updater_when_external_updater_is_configured()
|
||||
{
|
||||
Mocker.GetMock<IDeploymentInfoProvider>().Setup(x => x.IsExternalUpdateMechanism).Returns(true);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
|
||||
Mocker.GetMock<IProcessProvider>()
|
||||
.Verify(c => c.Start(It.IsAny<string>(), It.Is<string>(s => s.StartsWith("12")), null, null, null), Times.Never());
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
|
||||
@@ -127,6 +127,8 @@ namespace NzbDrone.Core.Applications
|
||||
|
||||
private void SyncIndexers(List<IApplication> applications, List<IndexerDefinition> indexers, bool removeRemote = false, bool forceSync = false)
|
||||
{
|
||||
var sortedIndexers = indexers.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
foreach (var app in applications)
|
||||
{
|
||||
var indexerMappings = _appIndexerMapService.GetMappingsForApp(app.Definition.Id);
|
||||
@@ -157,7 +159,7 @@ namespace NzbDrone.Core.Applications
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var indexer in indexers)
|
||||
foreach (var indexer in sortedIndexers)
|
||||
{
|
||||
var definition = indexer;
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Lidarr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Lidarr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Lidarr URL is invalid, Prowlarr cannot connect to Lidarr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -166,6 +166,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Radarr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Radarr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Radarr URL is invalid, Prowlarr cannot connect to Radarr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -179,6 +179,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Readarr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Readarr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Readarr URL is invalid, Prowlarr cannot connect to Readarr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -153,6 +153,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Sonarr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Sonarr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Sonarr URL is invalid, Prowlarr cannot connect to Sonarr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -166,6 +166,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Whisparr cannot connect to Prowlarr"));
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "Whisparr returned redirect and is invalid");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Whisparr URL is invalid, Prowlarr cannot connect to Whisparr - are you missing a URL base?"));
|
||||
break;
|
||||
|
||||
@@ -151,6 +151,7 @@ namespace NzbDrone.Core.Applications.Whisparr
|
||||
_logger.Error(ex, "Invalid Request");
|
||||
break;
|
||||
case HttpStatusCode.SeeOther:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
|
||||
@@ -25,8 +25,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings);
|
||||
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
|
||||
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
|
||||
void PauseTorrent(string hash, QBittorrentSettings settings);
|
||||
void ResumeTorrent(string hash, QBittorrentSettings settings);
|
||||
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
request.AddFormParameter("paused", false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
}
|
||||
@@ -176,7 +176,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
request.AddFormParameter("paused", false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
}
|
||||
@@ -212,7 +212,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel")
|
||||
.Post()
|
||||
@@ -254,7 +254,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -263,22 +263,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/pause")
|
||||
.Post()
|
||||
.AddFormParameter("hash", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void ResumeTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/resume")
|
||||
.Post()
|
||||
.AddFormParameter("hash", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/setForceStart")
|
||||
|
||||
@@ -246,14 +246,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
request.AddFormParameter("category", category);
|
||||
}
|
||||
|
||||
// Note: ForceStart is handled by separate api call
|
||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
|
||||
// Avoid extraneous API version check if initial state is ForceStart
|
||||
if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop)
|
||||
{
|
||||
request.AddFormParameter("paused", false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
var stoppedParameterName = GetApiVersion(settings) >= new Version(2, 11, 0) ? "stopped" : "paused";
|
||||
|
||||
// Note: ForceStart is handled by separate api call
|
||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Start)
|
||||
{
|
||||
request.AddFormParameter(stoppedParameterName, false);
|
||||
}
|
||||
else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop)
|
||||
{
|
||||
request.AddFormParameter(stoppedParameterName, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.SequentialOrder)
|
||||
@@ -291,7 +297,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -313,7 +319,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict)
|
||||
if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -322,22 +328,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/pause")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void ResumeTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/resume")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
public enum QBittorrentState
|
||||
{
|
||||
[FieldOption(Label = "Started")]
|
||||
Start = 0,
|
||||
|
||||
[FieldOption(Label = "Force Started")]
|
||||
ForceStart = 1,
|
||||
Pause = 2
|
||||
|
||||
[FieldOption(Label = "Stopped")]
|
||||
Stop = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
if (latestAvailable != null)
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
BuildInfo.BuildDateTime.Before(DateTime.UtcNow.AddDays(-180)) ? HealthCheckResult.Error : HealthCheckResult.Warning,
|
||||
_localizationService.GetLocalizedString("UpdateAvailableHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "version", $"v{latestAvailable.Version}" }
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
return new XElement(feedNamespace + "attr", new XAttribute("name", name), new XAttribute("value", value));
|
||||
}
|
||||
|
||||
public string ToXml(DownloadProtocol protocol)
|
||||
public string ToXml(DownloadProtocol protocol, bool preferMagnetUrl = false)
|
||||
{
|
||||
// IMPORTANT: We can't use Uri.ToString(), because it generates URLs without URL encode (links with unicode
|
||||
// characters are broken). We must use Uri.AbsoluteUri instead that handles encoding correctly
|
||||
@@ -73,6 +73,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
new XElement("title", "Prowlarr"),
|
||||
from r in Releases
|
||||
let t = (r as TorrentInfo) ?? new TorrentInfo()
|
||||
let downloadUrl = preferMagnetUrl ? t.MagnetUrl ?? r.DownloadUrl : r.DownloadUrl ?? t.MagnetUrl
|
||||
select new XElement("item",
|
||||
new XElement("title", RemoveInvalidXMLChars(r.Title)),
|
||||
new XElement("description", RemoveInvalidXMLChars(r.Description)),
|
||||
@@ -85,11 +86,11 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
r.InfoUrl == null ? null : new XElement("comments", r.InfoUrl),
|
||||
r.PublishDate == DateTime.MinValue ? new XElement("pubDate", XmlDateFormat(DateTime.Now)) : new XElement("pubDate", XmlDateFormat(r.PublishDate)),
|
||||
new XElement("size", r.Size),
|
||||
new XElement("link", r.DownloadUrl ?? t.MagnetUrl ?? string.Empty),
|
||||
new XElement("link", downloadUrl ?? string.Empty),
|
||||
r.Categories == null ? null : from c in r.Categories select new XElement("category", c.Id),
|
||||
new XElement(
|
||||
"enclosure",
|
||||
new XAttribute("url", r.DownloadUrl ?? t.MagnetUrl ?? string.Empty),
|
||||
new XAttribute("url", downloadUrl ?? string.Empty),
|
||||
r.Size == null ? null : new XAttribute("length", r.Size),
|
||||
new XAttribute("type", protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb")),
|
||||
r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol),
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -94,8 +95,10 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
|
||||
indexerList = response.Resource.Where(i => !_definitionBlocklist.Contains(i.File)).ToList();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Error while getting indexer definitions, fallback to reading from disk.");
|
||||
|
||||
var definitionFolder = Path.Combine(_appFolderInfo.AppDataFolder, "Definitions");
|
||||
|
||||
indexerList = ReadDefinitionsFromDisk(indexerList, definitionFolder);
|
||||
@@ -106,9 +109,9 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
indexerList = ReadDefinitionsFromDisk(indexerList, customDefinitionFolder);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Failed to Connect to Indexer Definition Server for Indexer listing");
|
||||
_logger.Error(ex, "Failed to Connect to Indexer Definition Server for Indexer listing");
|
||||
}
|
||||
|
||||
return indexerList;
|
||||
@@ -116,7 +119,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
public CardigannDefinition GetCachedDefinition(string fileKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileKey))
|
||||
if (string.IsNullOrWhiteSpace(fileKey))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileKey));
|
||||
}
|
||||
@@ -172,7 +175,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
private CardigannDefinition GetUncachedDefinition(string fileKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileKey))
|
||||
if (string.IsNullOrWhiteSpace(fileKey))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileKey));
|
||||
}
|
||||
@@ -220,9 +223,24 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
private CardigannDefinition GetHttpDefinition(string id)
|
||||
{
|
||||
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}");
|
||||
var response = _httpClient.Get(request);
|
||||
var definition = _deserializer.Deserialize<CardigannDefinition>(response.Content);
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
CardigannDefinition definition;
|
||||
|
||||
try
|
||||
{
|
||||
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}");
|
||||
var response = _httpClient.Get(request);
|
||||
|
||||
definition = _deserializer.Deserialize<CardigannDefinition>(response.Content);
|
||||
}
|
||||
catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new Exception($"Indexer definition for '{id}' does not exist.", ex);
|
||||
}
|
||||
|
||||
return CleanIndexerDefinition(definition);
|
||||
}
|
||||
|
||||
@@ -227,7 +227,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList();
|
||||
|
||||
if (queryCats.Any() && searchCriteria is TvSearchCriteria { Season: > 0 })
|
||||
{
|
||||
// Avoid searching for specials if it's a non-zero season search
|
||||
queryCats.RemoveAll(cat => cat is "anime[tv_special]" or "anime[ova]" or "anime[ona]" or "anime[dvd_special]" or "anime[bd_special]");
|
||||
}
|
||||
|
||||
if (queryCats.Any())
|
||||
{
|
||||
@@ -246,9 +252,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
searchUrl += "?" + parameters.GetQueryString();
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
|
||||
yield return request;
|
||||
yield return new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
}
|
||||
|
||||
private static string CleanSearchTerm(string term)
|
||||
|
||||
@@ -33,6 +33,12 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
throw new RequestLimitReachedException(indexerResponse, "API Request Limit Reached");
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
STJson.TryDeserialize<AvistazErrorResponse>(indexerResponse.HttpResponse.Content, out var errorResponse);
|
||||
throw new IndexerAuthException(errorResponse?.Message ?? "Unauthorized request to indexer");
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
|
||||
@@ -27,16 +27,16 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "IndexerAvistazSettingsUsernameHelpText", HelpTextWarning = "IndexerAvistazSettingsUsernameHelpTextWarning", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "IndexerAvistazSettingsPasswordHelpText", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
[FieldDefinition(4, Label = "PID", HelpText = "IndexerAvistazSettingsPidHelpText", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Pid { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech only")]
|
||||
[FieldDefinition(5, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox, HelpText = "IndexerAvistazSettingsFreeleechOnlyHelpText")]
|
||||
public bool FreeleechOnly { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new BeyondHDParser(Capabilities.Categories);
|
||||
return new BeyondHDParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
|
||||
@@ -227,10 +227,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class BeyondHDParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly BeyondHDSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public BeyondHDParser(IndexerCapabilitiesCategories categories)
|
||||
public BeyondHDParser(BeyondHDSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
@@ -264,6 +266,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
foreach (var row in jsonResponse.Results)
|
||||
{
|
||||
// Skip invalid results when freeleech or limited filtering is set
|
||||
if ((_settings.FreeleechOnly && !row.Freeleech) || (_settings.LimitedOnly && !row.Limited))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var details = row.InfoUrl;
|
||||
var link = row.DownloadLink;
|
||||
|
||||
@@ -318,8 +326,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public BeyondHDSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
RuleFor(c => c.RssKey).NotEmpty();
|
||||
RuleFor(c => c.ApiKey).NotEmpty().Length(32);
|
||||
RuleFor(c => c.RssKey).NotEmpty().Length(32);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,22 +39,22 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
|
||||
var indexerLogging = _configService.LogIndexerResponse;
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
if (indexerResponse.HttpResponse.HasHttpRedirect && indexerResponse.HttpResponse.RedirectUrl.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
if (indexerResponse.HttpResponse.HasHttpRedirect)
|
||||
_logger.Warn("Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl);
|
||||
|
||||
if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("/login.php"))
|
||||
{
|
||||
_logger.Warn("Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl);
|
||||
|
||||
if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("/login.php"))
|
||||
{
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from indexer request");
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from indexer request");
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
var loginUrl = ResolvePath(login.Path).ToString();
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables)).ToString();
|
||||
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
@@ -253,7 +253,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
}
|
||||
else if (login.Method == "form")
|
||||
{
|
||||
var loginUrl = ResolvePath(login.Path).ToString();
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables)).ToString();
|
||||
|
||||
var queryCollection = new NameValueCollection();
|
||||
var pairs = new Dictionary<string, string>();
|
||||
@@ -534,7 +534,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
var loginUrl = ResolvePath(login.Path + "?" + queryCollection.GetQueryString()).ToString();
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables) + "?" + queryCollection.GetQueryString()).ToString();
|
||||
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
@@ -563,7 +563,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
else if (login.Method == "oneurl")
|
||||
{
|
||||
var oneUrl = ApplyGoTemplateText(login.Inputs["oneurl"]);
|
||||
var loginUrl = ResolvePath(login.Path + oneUrl).ToString();
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables) + oneUrl).ToString();
|
||||
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
@@ -639,7 +639,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
var variables = GetBaseTemplateVariables();
|
||||
var headers = ParseCustomHeaders(_definition.Login?.Headers ?? _definition.Search?.Headers, variables);
|
||||
|
||||
var loginUrl = ResolvePath(login.Path);
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables));
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(loginUrl.AbsoluteUri)
|
||||
{
|
||||
@@ -700,7 +700,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
var captchaElement = landingResultDocument.QuerySelector(captcha.Selector);
|
||||
if (captchaElement != null)
|
||||
{
|
||||
var loginUrl = ResolvePath(login.Path);
|
||||
var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables));
|
||||
var captchaUrl = ResolvePath(captchaElement.GetAttribute("src"), loginUrl);
|
||||
|
||||
var request = new HttpRequestBuilder(captchaUrl.ToString())
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
{
|
||||
public class CardigannSettingsValidator : NoAuthSettingsValidator<CardigannSettings>
|
||||
{
|
||||
public CardigannSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.DefinitionFile).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class CardigannSettings : NoAuthTorrentBaseSettings
|
||||
{
|
||||
private static readonly CardigannSettingsValidator Validator = new ();
|
||||
|
||||
public CardigannSettings()
|
||||
{
|
||||
ExtraFieldData = new Dictionary<string, object>();
|
||||
@@ -15,5 +27,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||
public string DefinitionFile { get; set; }
|
||||
|
||||
public Dictionary<string, object> ExtraFieldData { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,8 @@ public class FileList : TorrentIndexerBase<FileListSettings>
|
||||
caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.Movies3D, "Filme 3D");
|
||||
caps.Categories.AddCategoryMapping(26, NewznabStandardCategory.MoviesBluRay, "Filme 4K Blu-Ray");
|
||||
caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.TVUHD, "Seriale 4K");
|
||||
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.MoviesForeign, "RO Dubbed");
|
||||
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.TVForeign, "RO Dubbed");
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public class FileListParser : IParseIndexerResponse
|
||||
InfoUrl = GetInfoUrl(id),
|
||||
Seeders = row.Seeders,
|
||||
Peers = row.Leechers + row.Seeders,
|
||||
PublishDate = DateTime.Parse(row.UploadDate + " +0300", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||
PublishDate = DateTime.Parse(row.UploadDate + " +0200", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||
Description = row.SmallDescription,
|
||||
Genres = row.SmallDescription.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||
ImdbId = imdbId,
|
||||
|
||||
@@ -151,7 +151,7 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
|
||||
|
||||
if (searchCriteria.Categories != null && searchCriteria.Categories.Any())
|
||||
{
|
||||
parameters.Set("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories)));
|
||||
parameters.Set("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList()));
|
||||
}
|
||||
|
||||
if (Settings.FreeleechOnly)
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var qc = new NameValueCollection();
|
||||
|
||||
foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories))
|
||||
foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct())
|
||||
{
|
||||
qc.Add(cat, string.Empty);
|
||||
}
|
||||
@@ -203,10 +203,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
// ipt uses sphinx, which supports boolean operators and grouping
|
||||
qc.Add("q", "+(" + imdbId + ")");
|
||||
|
||||
// search in description
|
||||
qc.Add("qf", "all");
|
||||
}
|
||||
|
||||
// changed from else if to if to support searching imdbid + season/episode in the same query
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
// changed from "else if" to "if" to support searching imdbid + season/episode in the same query
|
||||
if (term.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
// similar to above
|
||||
qc.Add("q", "+(" + term + ")");
|
||||
|
||||
348
src/NzbDrone.Core/Indexers/Definitions/Knaben.cs
Normal file
348
src/NzbDrone.Core/Indexers/Definitions/Knaben.cs
Normal file
@@ -0,0 +1,348 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using static Newtonsoft.Json.Formatting;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class Knaben : TorrentIndexerBase<NoAuthTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "Knaben";
|
||||
public override string[] IndexerUrls => new[] { "https://knaben.eu/" };
|
||||
public override string Description => "Knaben is a Public torrent meta-search engine";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Knaben(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new KnabenRequestGenerator(Capabilities);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new KnabenParser(Capabilities.Categories);
|
||||
}
|
||||
|
||||
private static IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1000000, NewznabStandardCategory.Audio, "Audio");
|
||||
caps.Categories.AddCategoryMapping(1001000, NewznabStandardCategory.AudioMP3, "MP3");
|
||||
caps.Categories.AddCategoryMapping(1002000, NewznabStandardCategory.AudioLossless, "Lossless");
|
||||
caps.Categories.AddCategoryMapping(1003000, NewznabStandardCategory.AudioAudiobook, "Audiobook");
|
||||
caps.Categories.AddCategoryMapping(1004000, NewznabStandardCategory.AudioVideo, "Audio Video");
|
||||
caps.Categories.AddCategoryMapping(1005000, NewznabStandardCategory.AudioOther, "Radio");
|
||||
caps.Categories.AddCategoryMapping(1006000, NewznabStandardCategory.AudioOther, "Audio Other");
|
||||
caps.Categories.AddCategoryMapping(2000000, NewznabStandardCategory.TV, "TV");
|
||||
caps.Categories.AddCategoryMapping(2001000, NewznabStandardCategory.TVHD, "TV HD");
|
||||
caps.Categories.AddCategoryMapping(2002000, NewznabStandardCategory.TVSD, "TV SD");
|
||||
caps.Categories.AddCategoryMapping(2003000, NewznabStandardCategory.TVUHD, "TV UHD");
|
||||
caps.Categories.AddCategoryMapping(2004000, NewznabStandardCategory.TVDocumentary, "Documentary");
|
||||
caps.Categories.AddCategoryMapping(2005000, NewznabStandardCategory.TVForeign, "TV Foreign");
|
||||
caps.Categories.AddCategoryMapping(2006000, NewznabStandardCategory.TVSport, "Sport");
|
||||
caps.Categories.AddCategoryMapping(2007000, NewznabStandardCategory.TVOther, "Cartoon");
|
||||
caps.Categories.AddCategoryMapping(2008000, NewznabStandardCategory.TVOther, "TV Other");
|
||||
caps.Categories.AddCategoryMapping(3000000, NewznabStandardCategory.Movies, "Movies");
|
||||
caps.Categories.AddCategoryMapping(3001000, NewznabStandardCategory.MoviesHD, "Movies HD");
|
||||
caps.Categories.AddCategoryMapping(3002000, NewznabStandardCategory.MoviesSD, "Movies SD");
|
||||
caps.Categories.AddCategoryMapping(3003000, NewznabStandardCategory.MoviesUHD, "Movies UHD");
|
||||
caps.Categories.AddCategoryMapping(3004000, NewznabStandardCategory.MoviesDVD, "Movies DVD");
|
||||
caps.Categories.AddCategoryMapping(3005000, NewznabStandardCategory.MoviesForeign, "Movies Foreign");
|
||||
caps.Categories.AddCategoryMapping(3006000, NewznabStandardCategory.MoviesForeign, "Movies Bollywood");
|
||||
caps.Categories.AddCategoryMapping(3007000, NewznabStandardCategory.Movies3D, "Movies 3D");
|
||||
caps.Categories.AddCategoryMapping(3008000, NewznabStandardCategory.MoviesOther, "Movies Other");
|
||||
caps.Categories.AddCategoryMapping(4000000, NewznabStandardCategory.PC, "PC");
|
||||
caps.Categories.AddCategoryMapping(4001000, NewznabStandardCategory.PCGames, "Games");
|
||||
caps.Categories.AddCategoryMapping(4002000, NewznabStandardCategory.PC0day, "Software");
|
||||
caps.Categories.AddCategoryMapping(4003000, NewznabStandardCategory.PCMac, "Mac");
|
||||
caps.Categories.AddCategoryMapping(4004000, NewznabStandardCategory.PCISO, "Unix");
|
||||
caps.Categories.AddCategoryMapping(5000000, NewznabStandardCategory.XXX, "XXX");
|
||||
caps.Categories.AddCategoryMapping(5001000, NewznabStandardCategory.XXXx264, "XXX Video");
|
||||
caps.Categories.AddCategoryMapping(5002000, NewznabStandardCategory.XXXImageSet, "XXX ImageSet");
|
||||
caps.Categories.AddCategoryMapping(5003000, NewznabStandardCategory.XXXOther, "XXX Games");
|
||||
caps.Categories.AddCategoryMapping(5004000, NewznabStandardCategory.XXXOther, "XXX Hentai");
|
||||
caps.Categories.AddCategoryMapping(5005000, NewznabStandardCategory.XXXOther, "XXX Other");
|
||||
caps.Categories.AddCategoryMapping(6000000, NewznabStandardCategory.TVAnime, "Anime");
|
||||
caps.Categories.AddCategoryMapping(6001000, NewznabStandardCategory.TVAnime, "Anime Subbed");
|
||||
caps.Categories.AddCategoryMapping(6002000, NewznabStandardCategory.TVAnime, "Anime Dubbed");
|
||||
caps.Categories.AddCategoryMapping(6003000, NewznabStandardCategory.TVAnime, "Anime Dual audio");
|
||||
caps.Categories.AddCategoryMapping(6004000, NewznabStandardCategory.TVAnime, "Anime Raw");
|
||||
caps.Categories.AddCategoryMapping(6005000, NewznabStandardCategory.AudioVideo, "Music Video");
|
||||
caps.Categories.AddCategoryMapping(6006000, NewznabStandardCategory.BooksOther, "Literature");
|
||||
caps.Categories.AddCategoryMapping(6007000, NewznabStandardCategory.AudioOther, "Music");
|
||||
caps.Categories.AddCategoryMapping(6008000, NewznabStandardCategory.TVAnime, "Anime non-english translated");
|
||||
caps.Categories.AddCategoryMapping(7000000, NewznabStandardCategory.Console, "Console");
|
||||
caps.Categories.AddCategoryMapping(7001000, NewznabStandardCategory.ConsolePS4, "PS4");
|
||||
caps.Categories.AddCategoryMapping(7002000, NewznabStandardCategory.ConsolePS3, "PS3");
|
||||
caps.Categories.AddCategoryMapping(7003000, NewznabStandardCategory.ConsolePS3, "PS2");
|
||||
caps.Categories.AddCategoryMapping(7004000, NewznabStandardCategory.ConsolePS3, "PS1");
|
||||
caps.Categories.AddCategoryMapping(7005000, NewznabStandardCategory.ConsolePSVita, "PS Vita");
|
||||
caps.Categories.AddCategoryMapping(7006000, NewznabStandardCategory.ConsolePSP, "PSP");
|
||||
caps.Categories.AddCategoryMapping(7007000, NewznabStandardCategory.ConsoleXBox360, "Xbox 360");
|
||||
caps.Categories.AddCategoryMapping(7008000, NewznabStandardCategory.ConsoleXBox, "Xbox");
|
||||
caps.Categories.AddCategoryMapping(7009000, NewznabStandardCategory.ConsoleNDS, "Switch");
|
||||
caps.Categories.AddCategoryMapping(7010000, NewznabStandardCategory.ConsoleNDS, "NDS");
|
||||
caps.Categories.AddCategoryMapping(7011000, NewznabStandardCategory.ConsoleWii, "Wii");
|
||||
caps.Categories.AddCategoryMapping(7012000, NewznabStandardCategory.ConsoleWiiU, "WiiU");
|
||||
caps.Categories.AddCategoryMapping(7013000, NewznabStandardCategory.Console3DS, "3DS");
|
||||
caps.Categories.AddCategoryMapping(7014000, NewznabStandardCategory.ConsoleWii, "GameCube");
|
||||
caps.Categories.AddCategoryMapping(7015000, NewznabStandardCategory.ConsoleOther, "Other");
|
||||
caps.Categories.AddCategoryMapping(8000000, NewznabStandardCategory.PCMobileOther, "Mobile");
|
||||
caps.Categories.AddCategoryMapping(8001000, NewznabStandardCategory.PCMobileAndroid, "Android");
|
||||
caps.Categories.AddCategoryMapping(8002000, NewznabStandardCategory.PCMobileiOS, "IOS");
|
||||
caps.Categories.AddCategoryMapping(8003000, NewznabStandardCategory.PCMobileOther, "PC Other");
|
||||
caps.Categories.AddCategoryMapping(9000000, NewznabStandardCategory.Books, "Books");
|
||||
caps.Categories.AddCategoryMapping(9001000, NewznabStandardCategory.BooksEBook, "EBooks");
|
||||
caps.Categories.AddCategoryMapping(9002000, NewznabStandardCategory.BooksComics, "Comics");
|
||||
caps.Categories.AddCategoryMapping(9003000, NewznabStandardCategory.BooksMags, "Magazines");
|
||||
caps.Categories.AddCategoryMapping(9004000, NewznabStandardCategory.BooksTechnical, "Technical");
|
||||
caps.Categories.AddCategoryMapping(9005000, NewznabStandardCategory.BooksOther, "Books Other");
|
||||
caps.Categories.AddCategoryMapping(10000000, NewznabStandardCategory.Other, "Other");
|
||||
caps.Categories.AddCategoryMapping(10001000, NewznabStandardCategory.OtherMisc, "Other Misc");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class KnabenRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private const string API_SEARCH_ENDPOINT = "https://api.knaben.eu/v1";
|
||||
|
||||
private readonly IndexerCapabilities _capabilities;
|
||||
|
||||
public KnabenRequestGenerator(IndexerCapabilities capabilities)
|
||||
{
|
||||
_capabilities = capabilities;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedSearchTerm));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedSearchTerm));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedTvSearchString));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedSearchTerm));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedSearchTerm));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> CreateRequest(SearchCriteriaBase searchCriteria, string searchTerm)
|
||||
{
|
||||
var body = new Dictionary<string, object>
|
||||
{
|
||||
{ "order_by", "date" },
|
||||
{ "order_direction", "desc" },
|
||||
{ "from", 0 },
|
||||
{ "size", 100 },
|
||||
{ "hide_unsafe", true }
|
||||
};
|
||||
|
||||
var searchQuery = searchTerm.Trim();
|
||||
|
||||
if (searchQuery.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
body.Add("search_type", "100%");
|
||||
body.Add("search_field", "title");
|
||||
body.Add("query", searchQuery);
|
||||
}
|
||||
|
||||
var categories = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
|
||||
if (categories is { Count: > 0 })
|
||||
{
|
||||
body.Add("categories", categories.Select(int.Parse).Distinct().ToArray());
|
||||
}
|
||||
|
||||
var request = new HttpRequest(API_SEARCH_ENDPOINT, HttpAccept.Json)
|
||||
{
|
||||
Headers =
|
||||
{
|
||||
ContentType = "application/json"
|
||||
},
|
||||
Method = HttpMethod.Post
|
||||
};
|
||||
request.SetContent(body.ToJson());
|
||||
request.ContentSummary = body.ToJson(None);
|
||||
|
||||
yield return new IndexerRequest(request);
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class KnabenParser : IParseIndexerResponse
|
||||
{
|
||||
private static readonly Regex DateTimezoneRegex = new (@"[+-]\d{2}:\d{2}$", RegexOptions.Compiled);
|
||||
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public KnabenParser(IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var indexerHttpResponse = indexerResponse.HttpResponse;
|
||||
|
||||
if (indexerHttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerHttpResponse.StatusCode} code from indexer request");
|
||||
}
|
||||
|
||||
if (!indexerHttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerHttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var jsonResponse = STJson.Deserialize<KnabenResponse>(indexerResponse.Content);
|
||||
|
||||
if (jsonResponse?.Hits == null)
|
||||
{
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
var rows = jsonResponse.Hits.Where(r => r.Seeders > 0).ToList();
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
// Not all entries have the TZ in the "date" field
|
||||
var publishDate = row.Date.IsNotNullOrWhiteSpace() && !DateTimezoneRegex.IsMatch(row.Date) ? $"{row.Date}+01:00" : row.Date;
|
||||
|
||||
var releaseInfo = new TorrentInfo
|
||||
{
|
||||
Guid = row.InfoUrl,
|
||||
Title = row.Title,
|
||||
InfoUrl = row.InfoUrl,
|
||||
DownloadUrl = row.DownloadUrl.IsNotNullOrWhiteSpace() ? row.DownloadUrl : null,
|
||||
MagnetUrl = row.MagnetUrl.IsNotNullOrWhiteSpace() ? row.MagnetUrl : null,
|
||||
Categories = row.CategoryIds.SelectMany(cat => _categories.MapTrackerCatToNewznab(cat.ToString())).Distinct().ToList(),
|
||||
InfoHash = row.InfoHash,
|
||||
Size = row.Size,
|
||||
Seeders = row.Seeders,
|
||||
Peers = row.Leechers + row.Seeders,
|
||||
PublishDate = DateTime.Parse(publishDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1
|
||||
};
|
||||
|
||||
releaseInfos.Add(releaseInfo);
|
||||
}
|
||||
|
||||
// order by date
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class KnabenResponse
|
||||
{
|
||||
public IReadOnlyCollection<KnabenRelease> Hits { get; init; } = Array.Empty<KnabenRelease>();
|
||||
}
|
||||
|
||||
internal sealed class KnabenRelease
|
||||
{
|
||||
public string Title { get; init; }
|
||||
|
||||
[JsonPropertyName("categoryId")]
|
||||
public IReadOnlyCollection<int> CategoryIds { get; init; } = Array.Empty<int>();
|
||||
|
||||
[JsonPropertyName("hash")]
|
||||
public string InfoHash { get; init; }
|
||||
|
||||
[JsonPropertyName("details")]
|
||||
public string InfoUrl { get; init; }
|
||||
|
||||
[JsonPropertyName("link")]
|
||||
public string DownloadUrl { get; init; }
|
||||
|
||||
public string MagnetUrl { get; init; }
|
||||
|
||||
[JsonPropertyName("bytes")]
|
||||
public long Size { get; init; }
|
||||
|
||||
public int Seeders { get; init; }
|
||||
|
||||
[JsonPropertyName("peers")]
|
||||
public int Leechers { get; init; }
|
||||
|
||||
public string Date { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
@@ -51,7 +52,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new MyAnonamouseParser(Settings, Capabilities.Categories, _httpClient, _cacheManager, _logger);
|
||||
return new MyAnonamouseParser(Definition, Settings, Capabilities.Categories, _httpClient, _cacheManager, _logger);
|
||||
}
|
||||
|
||||
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||
@@ -374,6 +375,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class MyAnonamouseParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly ProviderDefinition _definition;
|
||||
private readonly MyAnonamouseSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly IIndexerHttpClient _httpClient;
|
||||
@@ -386,12 +388,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
"Elite VIP"
|
||||
};
|
||||
|
||||
public MyAnonamouseParser(MyAnonamouseSettings settings,
|
||||
public MyAnonamouseParser(ProviderDefinition definition,
|
||||
MyAnonamouseSettings settings,
|
||||
IndexerCapabilitiesCategories categories,
|
||||
IIndexerHttpClient httpClient,
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_definition = definition;
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_httpClient = httpClient;
|
||||
@@ -543,7 +547,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
_logger.Debug("Fetching user data: {0}", request.Url.FullUri);
|
||||
|
||||
var response = _httpClient.Get(request);
|
||||
var response = _httpClient.ExecuteProxied(request, _definition);
|
||||
var jsonResponse = JsonConvert.DeserializeObject<MyAnonamouseUserDataResponse>(response.Content);
|
||||
|
||||
return jsonResponse.UserClass?.Trim();
|
||||
|
||||
@@ -20,6 +20,7 @@ using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
[Obsolete("Site has removed API access.")]
|
||||
public class NzbIndex : UsenetIndexerBase<NzbIndexSettings>
|
||||
{
|
||||
public override string Name => "NZBIndex";
|
||||
|
||||
@@ -73,6 +73,6 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
public class PassThePopcornFlag : IndexerFlag
|
||||
{
|
||||
public static IndexerFlag Golden => new ("golden", "Release follows Golden Popcorn quality rules");
|
||||
public static IndexerFlag Approved => new ("approved", "Release approved by PTP");
|
||||
public static IndexerFlag Approved => new ("approved", "Release approved by PTP staff");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,11 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
parameters.Set("freetorrent", "1");
|
||||
}
|
||||
|
||||
if (_settings.GoldenPopcornOnly)
|
||||
{
|
||||
parameters.Set("scene", "2");
|
||||
}
|
||||
|
||||
var queryCats = _capabilities.Categories
|
||||
.MapTorznabCapsToTrackers(searchCriteria.Categories)
|
||||
.Select(int.Parse)
|
||||
|
||||
@@ -27,6 +27,9 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
[FieldDefinition(4, Label = "IndexerSettingsFreeleechOnly", HelpText = "IndexerPassThePopcornSettingsFreeleechOnlyHelpText", Type = FieldType.Checkbox)]
|
||||
public bool FreeleechOnly { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "IndexerPassThePopcornSettingsGoldenPopcornOnly", HelpText = "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText", Type = FieldType.Checkbox, Advanced = true)]
|
||||
public bool GoldenPopcornOnly { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -63,5 +63,8 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Number, Label = "IndexerSettingsPackSeedTime", HelpText = "IndexerSettingsPackSeedTimeIndexerHelpText", Unit = "minutes", Advanced = true)]
|
||||
public int? PackSeedTime { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsPreferMagnetUrl", HelpText = "IndexerSettingsPreferMagnetUrlHelpText", Advanced = true)]
|
||||
public bool PreferMagnetUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using MonoTorrent;
|
||||
using NLog;
|
||||
@@ -22,10 +23,10 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
Torrent.Load(fileData);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug("Invalid torrent file contents: {0}", Encoding.ASCII.GetString(fileData));
|
||||
throw;
|
||||
throw new NotSupportedException($"Invalid torrent file contents. Reason: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,33 +60,36 @@ namespace NzbDrone.Core.Instrumentation
|
||||
{
|
||||
try
|
||||
{
|
||||
var log = new Log();
|
||||
log.Time = logEvent.TimeStamp;
|
||||
log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage);
|
||||
|
||||
log.Logger = logEvent.LoggerName;
|
||||
var log = new Log
|
||||
{
|
||||
Time = logEvent.TimeStamp,
|
||||
Logger = logEvent.LoggerName,
|
||||
Level = logEvent.Level.Name
|
||||
};
|
||||
|
||||
if (log.Logger.StartsWith("NzbDrone."))
|
||||
{
|
||||
log.Logger = log.Logger.Remove(0, 9);
|
||||
}
|
||||
|
||||
var message = logEvent.FormattedMessage;
|
||||
|
||||
if (logEvent.Exception != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(log.Message))
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
log.Message = logEvent.Exception.Message;
|
||||
message = logEvent.Exception.Message;
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Message += ": " + logEvent.Exception.Message;
|
||||
message += ": " + logEvent.Exception.Message;
|
||||
}
|
||||
|
||||
log.Exception = logEvent.Exception.ToString();
|
||||
log.Exception = CleanseLogMessage.Cleanse(logEvent.Exception.ToString());
|
||||
log.ExceptionType = logEvent.Exception.GetType().ToString();
|
||||
}
|
||||
|
||||
log.Level = logEvent.Level.Name;
|
||||
log.Message = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
var connectionInfo = _connectionStringFactory.LogDbConnection;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user