mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
119 Commits
v1.25.2.47
...
v1.30.2.49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f7ac7d7d6 | ||
|
|
8ac68240ad | ||
|
|
b463a3f54b | ||
|
|
e15e57329e | ||
|
|
d8354408a4 | ||
|
|
6d2d49f7bd | ||
|
|
37610eec40 | ||
|
|
ed51208116 | ||
|
|
26e4dcad65 | ||
|
|
6eb21a02a1 | ||
|
|
8c2d5a404d | ||
|
|
3b83a00eaf | ||
|
|
a5a86a6f86 | ||
|
|
e7ed09a43d | ||
|
|
547bc2e58c | ||
|
|
8eb674c8d7 | ||
|
|
2c3621d25e | ||
|
|
2648f2c639 | ||
|
|
f4d621063b | ||
|
|
73494c462c | ||
|
|
36f6896f30 | ||
|
|
e01741a69e | ||
|
|
1dbff1235e | ||
|
|
1a9ad6b363 | ||
|
|
c88249300c | ||
|
|
7b8e352d87 | ||
|
|
81f7a6cbab | ||
|
|
523e46af2a | ||
|
|
2b4a6def2a | ||
|
|
9097c0ef6d | ||
|
|
4321c1d40c | ||
|
|
bb2548a08d | ||
|
|
3a9b841fad | ||
|
|
31203d1370 | ||
|
|
c8a910eaf4 | ||
|
|
9ab3c3e6c7 | ||
|
|
4659cb706a | ||
|
|
500759bf1f | ||
|
|
43c7c43257 | ||
|
|
9c2fced391 | ||
|
|
52ec5b6ff6 | ||
|
|
b46e657976 | ||
|
|
51fd30ba10 | ||
|
|
5fbb347108 | ||
|
|
54d3d44620 | ||
|
|
5ca18683ca | ||
|
|
6bdf5f5d69 | ||
|
|
7cba7152f1 | ||
|
|
cf012eb001 | ||
|
|
6b8a7993ff | ||
|
|
c6440bb21b | ||
|
|
b95eac98b9 | ||
|
|
0eb19ce834 | ||
|
|
4b8016d95d | ||
|
|
31d8d2419a | ||
|
|
d29ccd7749 | ||
|
|
e789f4ec54 | ||
|
|
58d495d618 | ||
|
|
f3328863e1 | ||
|
|
a23d792781 | ||
|
|
f066cf399d | ||
|
|
61e863cb31 | ||
|
|
b2afbc6872 | ||
|
|
aace65f88e | ||
|
|
9ab2d8b444 | ||
|
|
bc314061ef | ||
|
|
87b3dcd780 | ||
|
|
f3b99f68f6 | ||
|
|
c4a90e8ba4 | ||
|
|
41320ca2dc | ||
|
|
b8b32f8708 | ||
|
|
30c4bb24e8 | ||
|
|
b447db5d08 | ||
|
|
299001a513 | ||
|
|
2871f1f2a2 | ||
|
|
a9b93df0c9 | ||
|
|
2726787ee9 | ||
|
|
b917932f19 | ||
|
|
06ae85e6d1 | ||
|
|
b1c7e98664 | ||
|
|
62479737a7 | ||
|
|
8e69415d64 | ||
|
|
222dfb1821 | ||
|
|
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 |
@@ -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,13 +9,13 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.25.2'
|
||||
majorVersion: '1.30.2'
|
||||
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'
|
||||
@@ -1169,12 +1169,12 @@ stages:
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@2
|
||||
- task: SonarCloudPrepare@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'prowlarr'
|
||||
scannerMode: 'MSBuild'
|
||||
scannerMode: 'dotnet'
|
||||
projectKey: 'Prowlarr_Prowlarr'
|
||||
projectName: 'Prowlarr'
|
||||
projectVersion: '$(prowlarrVersion)'
|
||||
@@ -1187,10 +1187,10 @@ stages:
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@2
|
||||
- task: SonarCloudAnalyze@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@5
|
||||
- task: reportgenerator@5.3.11
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
|
||||
@@ -25,6 +25,7 @@ module.exports = (env) => {
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
target: 'web',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
@@ -169,7 +170,7 @@ module.exports = (env) => {
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
corejs: '3.39'
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -257,6 +257,7 @@ class HistoryRow extends Component {
|
||||
key={parameter.key}
|
||||
title={parameter.title}
|
||||
value={data[parameter.key]}
|
||||
queryType={data.queryType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { HistoryQueryType } from 'typings/History';
|
||||
import styles from './HistoryRowParameter.css';
|
||||
|
||||
interface HistoryRowParameterProps {
|
||||
title: string;
|
||||
value: string;
|
||||
queryType: HistoryQueryType;
|
||||
}
|
||||
|
||||
function HistoryRowParameter(props: HistoryRowParameterProps) {
|
||||
const { title, value } = props;
|
||||
const { title, value, queryType } = props;
|
||||
|
||||
const type = title.toLowerCase();
|
||||
|
||||
@@ -18,7 +20,13 @@ function HistoryRowParameter(props: HistoryRowParameterProps) {
|
||||
link = <Link to={`https://imdb.com/title/${value}/`}>{value}</Link>;
|
||||
} else if (type === 'tmdb') {
|
||||
link = (
|
||||
<Link to={`https://www.themoviedb.org/movie/${value}`}>{value}</Link>
|
||||
<Link
|
||||
to={`https://www.themoviedb.org/${
|
||||
queryType === 'tvsearch' ? 'tv' : 'movie'
|
||||
}/${value}`}
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
);
|
||||
} else if (type === 'tvdb') {
|
||||
link = (
|
||||
|
||||
@@ -68,6 +68,7 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) {
|
||||
key={parameter.key}
|
||||
title={parameter.title}
|
||||
value={data[parameter.key as keyof HistoryData].toString()}
|
||||
queryType={data.queryType}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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,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);
|
||||
@@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise<boolean> {
|
||||
translations = data.Strings;
|
||||
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export type HistoryQueryType =
|
||||
| 'search'
|
||||
| 'tvsearch'
|
||||
| 'movie'
|
||||
| 'book'
|
||||
| 'music';
|
||||
|
||||
export interface HistoryData {
|
||||
source: string;
|
||||
host: string;
|
||||
@@ -7,7 +14,7 @@ export interface HistoryData {
|
||||
offset: number;
|
||||
elapsedTime: number;
|
||||
query: string;
|
||||
queryType: string;
|
||||
queryType: HistoryQueryType;
|
||||
}
|
||||
|
||||
interface History extends ModelBase {
|
||||
|
||||
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;
|
||||
|
||||
28
package.json
28
package.json
@@ -23,10 +23,10 @@
|
||||
"defaults"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/fontawesome-free": "6.7.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
@@ -81,30 +81,30 @@
|
||||
"redux-thunk": "2.4.2",
|
||||
"reselect": "4.1.8",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.8",
|
||||
"@babel/eslint-parser": "7.25.8",
|
||||
"@babel/plugin-proposal-export-default-from": "7.25.8",
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/eslint-parser": "7.25.9",
|
||||
"@babel/plugin-proposal-export-default-from": "7.25.9",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.25.8",
|
||||
"@babel/preset-react": "7.25.7",
|
||||
"@babel/preset-typescript": "7.25.7",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/react-document-title": "2.0.10",
|
||||
"@types/react-router-dom": "5.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",
|
||||
"@typescript-eslint/eslint-plugin": "8.18.1",
|
||||
"@typescript-eslint/parser": "8.18.1",
|
||||
"are-you-es5": "2.1.2",
|
||||
"autoprefixer": "10.4.20",
|
||||
"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.1",
|
||||
"core-js": "3.39.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.57.1",
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
||||
'$(RuntimeIdentifier)' == ''">
|
||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier>osx-$(Architecture)</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -21,9 +21,28 @@ namespace NzbDrone.Common.Test.ExtensionTests
|
||||
[TestCase("1.2.3.4")]
|
||||
[TestCase("172.55.0.1")]
|
||||
[TestCase("192.55.0.1")]
|
||||
[TestCase("100.64.0.1")]
|
||||
[TestCase("100.127.255.254")]
|
||||
public void should_return_false_for_public_ip_address(string ipAddress)
|
||||
{
|
||||
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("100.64.0.1")]
|
||||
[TestCase("100.127.255.254")]
|
||||
[TestCase("100.100.100.100")]
|
||||
public void should_return_true_for_cgnat_ip_address(string ipAddress)
|
||||
{
|
||||
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("1.2.3.4")]
|
||||
[TestCase("192.168.5.1")]
|
||||
[TestCase("100.63.255.255")]
|
||||
[TestCase("100.128.0.0")]
|
||||
public void should_return_false_for_non_cgnat_ip_address(string ipAddress)
|
||||
{
|
||||
IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,17 +42,18 @@ namespace NzbDrone.Common
|
||||
|
||||
public void CreateZip(string path, IEnumerable<string> files)
|
||||
{
|
||||
using (var zipFile = ZipFile.Create(path))
|
||||
_logger.Debug("Creating archive {0}", path);
|
||||
|
||||
using var zipFile = ZipFile.Create(path);
|
||||
|
||||
zipFile.BeginUpdate();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
zipFile.BeginUpdate();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
zipFile.Add(file, Path.GetFileName(file));
|
||||
}
|
||||
|
||||
zipFile.CommitUpdate();
|
||||
zipFile.Add(file, Path.GetFileName(file));
|
||||
}
|
||||
|
||||
zipFile.CommitUpdate();
|
||||
}
|
||||
|
||||
private void ExtractZip(string compressedFile, string destination)
|
||||
|
||||
@@ -189,6 +189,25 @@ namespace NzbDrone.Common.Disk
|
||||
}
|
||||
|
||||
var fi = new FileInfo(path);
|
||||
|
||||
try
|
||||
{
|
||||
// If the file is a symlink, resolve the target path and get the size of the target file.
|
||||
if (fi.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||
{
|
||||
var targetPath = fi.ResolveLinkTarget(true)?.FullName;
|
||||
|
||||
if (targetPath != null)
|
||||
{
|
||||
fi = new FileInfo(targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.Trace(ex, "Unable to resolve symlink target for {0}", path);
|
||||
}
|
||||
|
||||
return fi.Length;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,18 +39,24 @@ namespace NzbDrone.Common.Extensions
|
||||
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
||||
{
|
||||
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
||||
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||
var isLinkLocal = ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||
|
||||
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
||||
bool IsClassA() => ipv4Bytes[0] == 10;
|
||||
var isClassA = ipv4Bytes[0] == 10;
|
||||
|
||||
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
||||
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||
var isClassB = ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||
|
||||
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
||||
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||
var isClassC = ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||
|
||||
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
|
||||
return isLinkLocal || isClassA || isClassC || isClassB;
|
||||
}
|
||||
|
||||
public static bool IsCgnatIpAddress(this IPAddress ipAddress)
|
||||
{
|
||||
var bytes = ipAddress.GetAddressBytes();
|
||||
return bytes.Length == 4 && bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
o.Environment = BuildInfo.Branch;
|
||||
|
||||
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
|
||||
o.AutoSessionTracking = true;
|
||||
o.AutoSessionTracking = false;
|
||||
|
||||
// Caches files in the event device is offline
|
||||
// Sentry creates a 'sentry' sub directory, no need to concat here
|
||||
@@ -148,7 +148,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
_debounce = new SentryDebounce();
|
||||
|
||||
// initialize to true and reconfigure later
|
||||
// Otherwise it will default to false and any errors occuring
|
||||
// Otherwise it will default to false and any errors occurring
|
||||
// before config file gets read will not be filtered
|
||||
FilterEvents = true;
|
||||
SentryEnabled = true;
|
||||
@@ -207,9 +207,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
|
||||
private void OnError(Exception ex)
|
||||
{
|
||||
var webException = ex as WebException;
|
||||
|
||||
if (webException != null)
|
||||
if (ex is WebException webException)
|
||||
{
|
||||
var response = webException.Response as HttpWebResponse;
|
||||
var statusCode = response?.StatusCode;
|
||||
|
||||
@@ -6,4 +6,5 @@ public class AuthOptions
|
||||
public bool? Enabled { get; set; }
|
||||
public string Method { get; set; }
|
||||
public string Required { get; set; }
|
||||
public bool? TrustCgnatIpAddresses { get; set; }
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ namespace NzbDrone.Common.Processes
|
||||
processInfo = new ProcessInfo();
|
||||
processInfo.Id = process.Id;
|
||||
processInfo.Name = process.ProcessName;
|
||||
processInfo.StartPath = process.MainModule.FileName;
|
||||
processInfo.StartPath = process.MainModule?.FileName;
|
||||
|
||||
if (process.Id != GetCurrentProcessId() && process.HasExited)
|
||||
{
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageReference Include="IPAddressRange" Version="6.0.0" />
|
||||
<PackageReference Include="IPAddressRange" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.3.3" />
|
||||
<PackageReference Include="NLog" Version="5.3.4" />
|
||||
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.12" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.8" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.9" />
|
||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.10" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore.Converters;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Converters;
|
||||
|
||||
[TestFixture]
|
||||
public class TimeSpanConverterFixture : CoreTest<TimeSpanConverter>
|
||||
{
|
||||
private SQLiteParameter _param;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_param = new SQLiteParameter();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_string_when_saving_timespan_to_db()
|
||||
{
|
||||
var span = TimeSpan.FromMilliseconds(10);
|
||||
|
||||
Subject.SetValue(_param, span);
|
||||
_param.Value.Should().Be(span.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_timespan_when_getting_string_from_db()
|
||||
{
|
||||
var span = TimeSpan.FromMilliseconds(10);
|
||||
|
||||
Subject.Parse(span.ToString()).Should().Be(span);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_zero_timespan_for_db_null_value_when_getting_from_db()
|
||||
{
|
||||
Subject.Parse(null).Should().Be(TimeSpan.Zero);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore;
|
||||
|
||||
[TestFixture]
|
||||
public class DatabaseVersionParserFixture
|
||||
{
|
||||
[TestCase("3.44.2", 3, 44, 2)]
|
||||
public void should_parse_sqlite_database_version(string serverVersion, int majorVersion, int minorVersion, int buildVersion)
|
||||
{
|
||||
var version = DatabaseVersionParser.ParseServerVersion(serverVersion);
|
||||
|
||||
version.Should().NotBeNull();
|
||||
version.Major.Should().Be(majorVersion);
|
||||
version.Minor.Should().Be(minorVersion);
|
||||
version.Build.Should().Be(buildVersion);
|
||||
}
|
||||
|
||||
[TestCase("14.8 (Debian 14.8-1.pgdg110+1)", 14, 8, null)]
|
||||
[TestCase("16.3 (Debian 16.3-1.pgdg110+1)", 16, 3, null)]
|
||||
[TestCase("16.3 - Percona Distribution", 16, 3, null)]
|
||||
[TestCase("17.0 - Percona Server", 17, 0, null)]
|
||||
public void should_parse_postgres_database_version(string serverVersion, int majorVersion, int minorVersion, int? buildVersion)
|
||||
{
|
||||
var version = DatabaseVersionParser.ParseServerVersion(serverVersion);
|
||||
|
||||
version.Should().NotBeNull();
|
||||
version.Major.Should().Be(majorVersion);
|
||||
version.Minor.Should().Be(minorVersion);
|
||||
|
||||
if (buildVersion.HasValue)
|
||||
{
|
||||
version.Build.Should().Be(buildVersion.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
|
||||
|
||||
var fifthTorrentInfo = releases.ElementAt(28) as TorrentInfo;
|
||||
|
||||
fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]");
|
||||
fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS 2021 S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]");
|
||||
fifthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
fifthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/944509/download/somepass");
|
||||
fifthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/944509/group");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -46,8 +46,8 @@ namespace NzbDrone.Core.Test.IndexerTests.RedactedTests
|
||||
|
||||
torrentInfo.Title.Should().Be("Red Hot Chili Peppers - Californication (1999) [Album] [US / Reissue 2020] [FLAC 24bit Lossless / Vinyl]");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("https://redacted.ch/ajax.php?action=download&id=3892313");
|
||||
torrentInfo.InfoUrl.Should().Be("https://redacted.ch/torrents.php?id=16720&torrentid=3892313");
|
||||
torrentInfo.DownloadUrl.Should().Be("https://redacted.sh/ajax.php?action=download&id=3892313");
|
||||
torrentInfo.InfoUrl.Should().Be("https://redacted.sh/torrents.php?id=16720&torrentid=3892313");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-17 08:02:35"));
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Applications
|
||||
|
||||
foreach (var application in applications)
|
||||
{
|
||||
if (blockedApplications.TryGetValue(application.Definition.Id, out var blockedApplicationStatus))
|
||||
if (blockedApplications.TryGetValue(application.Definition.Id, out var blockedApplicationStatus) && blockedApplicationStatus.DisabledTill.HasValue)
|
||||
{
|
||||
_logger.Debug("Temporarily ignoring application {0} till {1} due to recent failures.", application.Definition.Name, blockedApplicationStatus.DisabledTill.Value.ToLocalTime());
|
||||
continue;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -66,12 +66,19 @@ namespace NzbDrone.Core.Backup
|
||||
{
|
||||
_logger.ProgressInfo("Starting Backup");
|
||||
|
||||
var backupFolder = GetBackupFolder(backupType);
|
||||
|
||||
_diskProvider.EnsureFolder(_backupTempFolder);
|
||||
_diskProvider.EnsureFolder(GetBackupFolder(backupType));
|
||||
_diskProvider.EnsureFolder(backupFolder);
|
||||
|
||||
if (!_diskProvider.FolderWritable(backupFolder))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable");
|
||||
}
|
||||
|
||||
var dateNow = DateTime.Now;
|
||||
var backupFilename = $"prowlarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
|
||||
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename);
|
||||
var backupPath = Path.Combine(backupFolder, backupFilename);
|
||||
|
||||
Cleanup();
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ namespace NzbDrone.Core.Configuration
|
||||
string PostgresPassword { get; }
|
||||
string PostgresMainDb { get; }
|
||||
string PostgresLogDb { get; }
|
||||
bool TrustCgnatIpAddresses { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
@@ -479,5 +480,7 @@ namespace NzbDrone.Core.Configuration
|
||||
SetValue("ApiKey", GenerateApiKey());
|
||||
_eventAggregator.PublishEvent(new ApiKeyChangedEvent());
|
||||
}
|
||||
|
||||
public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +183,12 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
|
||||
|
||||
public bool TrustCgnatIpAddresses
|
||||
{
|
||||
get { return GetValueBoolean("TrustCgnatIpAddresses", false); }
|
||||
set { SetValue("TrustCgnatIpAddresses", value); }
|
||||
}
|
||||
|
||||
private string GetValue(string key)
|
||||
{
|
||||
return GetValue(key, string.Empty);
|
||||
|
||||
@@ -2,18 +2,17 @@ using System;
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class DapperTimeSpanConverter : SqlMapper.TypeHandler<TimeSpan>
|
||||
{
|
||||
public override void SetValue(IDbDataParameter parameter, TimeSpan value)
|
||||
{
|
||||
parameter.Value = value.ToString();
|
||||
}
|
||||
namespace NzbDrone.Core.Datastore.Converters;
|
||||
|
||||
public override TimeSpan Parse(object value)
|
||||
{
|
||||
return TimeSpan.Parse((string)value);
|
||||
}
|
||||
public class TimeSpanConverter : SqlMapper.TypeHandler<TimeSpan>
|
||||
{
|
||||
public override void SetValue(IDbDataParameter parameter, TimeSpan value)
|
||||
{
|
||||
parameter.Value = value.ToString();
|
||||
}
|
||||
|
||||
public override TimeSpan Parse(object value)
|
||||
{
|
||||
return value is string str ? TimeSpan.Parse(str) : TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
@@ -52,9 +51,8 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using var db = _datamapperFactory();
|
||||
var dbConnection = db as DbConnection;
|
||||
var version = Regex.Replace(dbConnection.ServerVersion, @"\(.*?\)", "");
|
||||
|
||||
return new Version(version);
|
||||
return DatabaseVersionParser.ParseServerVersion(dbConnection.ServerVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs
Normal file
16
src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NzbDrone.Core.Datastore;
|
||||
|
||||
public static class DatabaseVersionParser
|
||||
{
|
||||
private static readonly Regex VersionRegex = new (@"^[^ ]+", RegexOptions.Compiled);
|
||||
|
||||
public static Version ParseServerVersion(string serverVersion)
|
||||
{
|
||||
var match = VersionRegex.Match(serverVersion);
|
||||
|
||||
return match.Success ? new Version(match.Value) : null;
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,6 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
SqlMapper.RemoveTypeMap(typeof(DateTime));
|
||||
SqlMapper.AddTypeHandler(new DapperUtcConverter());
|
||||
SqlMapper.AddTypeHandler(new DapperTimeSpanConverter());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<Dictionary<string, string>>());
|
||||
SqlMapper.AddTypeHandler(new CookieConverter());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());
|
||||
@@ -123,6 +122,9 @@ namespace NzbDrone.Core.Datastore
|
||||
SqlMapper.RemoveTypeMap(typeof(Guid));
|
||||
SqlMapper.RemoveTypeMap(typeof(Guid?));
|
||||
SqlMapper.AddTypeHandler(new GuidConverter());
|
||||
SqlMapper.RemoveTypeMap(typeof(TimeSpan));
|
||||
SqlMapper.RemoveTypeMap(typeof(TimeSpan?));
|
||||
SqlMapper.AddTypeHandler(new TimeSpanConverter());
|
||||
SqlMapper.AddTypeHandler(new CommandConverter());
|
||||
SqlMapper.AddTypeHandler(new SystemVersionConverter());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Net;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
@@ -208,7 +209,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
|
||||
private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false)
|
||||
{
|
||||
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
|
||||
var authKey = $"{requestBuilder.BaseUrl}:{settings.Password}";
|
||||
|
||||
var sessionId = _authSessionIDCache.Find(authKey);
|
||||
|
||||
@@ -220,24 +221,26 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
authLoginRequest.SuppressHttpError = true;
|
||||
|
||||
var response = _httpClient.Execute(authLoginRequest);
|
||||
if (response.StatusCode == HttpStatusCode.MovedPermanently)
|
||||
{
|
||||
var url = response.Headers.GetSingleValue("Location");
|
||||
|
||||
throw new DownloadClientException("Remote site redirected to " + url);
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
switch (response.StatusCode)
|
||||
{
|
||||
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
|
||||
case HttpStatusCode.MovedPermanently:
|
||||
var url = response.Headers.GetSingleValue("Location");
|
||||
|
||||
if (sessionId == null)
|
||||
{
|
||||
throw new DownloadClientException("Remote host did not return a Session Id.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
|
||||
throw new DownloadClientException("Remote site redirected to " + url);
|
||||
case HttpStatusCode.Forbidden:
|
||||
throw new DownloadClientException($"Failed to authenticate with Transmission. It may be necessary to add {BuildInfo.AppName}'s IP address to RPC whitelist.");
|
||||
case HttpStatusCode.Conflict:
|
||||
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
|
||||
|
||||
if (sessionId == null)
|
||||
{
|
||||
throw new DownloadClientException("Remote host did not return a Session Id.");
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
|
||||
}
|
||||
|
||||
_logger.Debug("Transmission authentication succeeded.");
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
foreach (var client in clients)
|
||||
{
|
||||
if (blockedClients.TryGetValue(client.Definition.Id, out var downloadClientStatus))
|
||||
if (blockedClients.TryGetValue(client.Definition.Id, out var downloadClientStatus) && downloadClientStatus.DisabledTill.HasValue)
|
||||
{
|
||||
_logger.Debug("Temporarily ignoring download client {0} till {1} due to recent failures.", client.Definition.Name, downloadClientStatus.DisabledTill.Value.ToLocalTime());
|
||||
continue;
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
private readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SystemTimeCheck(IHttpClient client, IProwlarrCloudRequestBuilder cloudRequestBuilder, ILocalizationService localizationService, Logger logger)
|
||||
public SystemTimeCheck(IHttpClient client, IProwlarrCloudRequestBuilder cloudRequestBuilder, Logger logger, ILocalizationService localizationService)
|
||||
: base(localizationService)
|
||||
{
|
||||
_client = client;
|
||||
@@ -29,19 +29,26 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
.Resource("/time")
|
||||
.Build();
|
||||
|
||||
var response = _client.Execute(request);
|
||||
var result = Json.Deserialize<ServiceTimeResponse>(response.Content);
|
||||
var systemTime = DateTime.UtcNow;
|
||||
|
||||
// +/- more than 1 day
|
||||
if (Math.Abs(result.DateTimeUtc.Subtract(systemTime).TotalDays) >= 1)
|
||||
try
|
||||
{
|
||||
_logger.Error("System time mismatch. SystemTime: {0} Expected Time: {1}. Update system time", systemTime, result.DateTimeUtc);
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("SystemTimeCheckMessage"));
|
||||
var request = _cloudRequestBuilder.Create()
|
||||
.Resource("/time")
|
||||
.Build();
|
||||
|
||||
var response = _client.Execute(request);
|
||||
var result = Json.Deserialize<ServiceTimeResponse>(response.Content);
|
||||
var systemTime = DateTime.UtcNow;
|
||||
|
||||
// +/- more than 1 day
|
||||
if (Math.Abs(result.DateTimeUtc.Subtract(systemTime).TotalDays) >= 1)
|
||||
{
|
||||
_logger.Error("System time mismatch. SystemTime: {0} Expected Time: {1}. Update system time", systemTime, result.DateTimeUtc);
|
||||
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("SystemTimeHealthCheckMessage"), "#system-time-off");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warn(e, "Unable to verify system time");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
|
||||
@@ -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}" }
|
||||
|
||||
@@ -301,6 +301,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
};
|
||||
private static readonly HashSet<string> ExcludedFileExtensions = new (StringComparer.OrdinalIgnoreCase) { ".mka", ".mds", ".md5", ".nfo", ".sfv", ".ass", ".mks", ".srt", ".ssa", ".sup", ".jpeg", ".jpg", ".png", ".otf", ".ttf" };
|
||||
|
||||
private static readonly string[] PropertiesSeparator = { " | ", " / " };
|
||||
|
||||
private readonly AnimeBytesSettings _settings;
|
||||
|
||||
public AnimeBytesParser(AnimeBytesSettings settings)
|
||||
@@ -324,6 +326,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var response = STJson.Deserialize<AnimeBytesResponse>(indexerResponse.Content);
|
||||
|
||||
if (response.Error.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", response.Error);
|
||||
}
|
||||
|
||||
if (response.Matches == 0)
|
||||
{
|
||||
return releaseInfos.ToArray();
|
||||
@@ -393,38 +400,48 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var minimumSeedTime = 259200 + (int)(size / (int)Math.Pow(1024, 3) * 18000);
|
||||
|
||||
var propertyList = WebUtility.HtmlDecode(torrent.Property)
|
||||
.Split(new[] { " | ", " / " }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Split(PropertiesSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
|
||||
propertyList.RemoveAll(p => ExcludedProperties.Any(p.ContainsIgnoreCase));
|
||||
var properties = propertyList.ToHashSet();
|
||||
|
||||
if (torrent.Files.Any(f => f.FileName.ContainsIgnoreCase("Remux")))
|
||||
{
|
||||
var resolutionProperty = properties.FirstOrDefault(RemuxResolutions.ContainsIgnoreCase);
|
||||
|
||||
if (resolutionProperty.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
properties.Add($"{resolutionProperty} Remux");
|
||||
}
|
||||
}
|
||||
|
||||
if (properties.Any(p => p.StartsWithIgnoreCase("M2TS")))
|
||||
if (properties.Any(p => p.StartsWith("M2TS", StringComparison.Ordinal)))
|
||||
{
|
||||
properties.Add("BR-DISK");
|
||||
}
|
||||
|
||||
if (_settings.ExcludeRaw &&
|
||||
properties.Any(p => p.StartsWithIgnoreCase("RAW") || p.Contains("BR-DISK")))
|
||||
var isBluRayDisk = properties.Any(p => p.Equals("RAW", StringComparison.Ordinal) || p.StartsWith("M2TS", StringComparison.Ordinal) || p.StartsWith("ISO", StringComparison.Ordinal));
|
||||
|
||||
if (_settings.ExcludeRaw && isBluRayDisk)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
properties = properties
|
||||
.Select(property =>
|
||||
{
|
||||
if (isBluRayDisk)
|
||||
{
|
||||
property = Regex.Replace(property, @"\b(H\.?265)\b", "HEVC", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
property = Regex.Replace(property, @"\b(H\.?264)\b", "AVC", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
if (torrent.Files.Any(f => f.FileName.ContainsIgnoreCase("Remux"))
|
||||
&& RemuxResolutions.ContainsIgnoreCase(property))
|
||||
{
|
||||
property += " Remux";
|
||||
}
|
||||
|
||||
return property;
|
||||
})
|
||||
.ToHashSet();
|
||||
|
||||
int? season = null;
|
||||
int? episode = null;
|
||||
|
||||
var releaseInfo = _settings.EnableSonarrCompatibility && categoryName == "Anime" ? "S01" : "";
|
||||
var editionTitle = torrent.EditionData.EditionTitle;
|
||||
var editionTitle = torrent.EditionData?.EditionTitle;
|
||||
|
||||
if (editionTitle.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
@@ -569,7 +586,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (_settings.UseFilenameForSingleEpisodes)
|
||||
{
|
||||
var files = torrent.Files;
|
||||
var files = torrent.Files.ToList();
|
||||
|
||||
if (files.Count > 1)
|
||||
{
|
||||
@@ -607,11 +624,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
var useYearInTitle = year is > 0 && torrent.Files.Any(f => f.FileName.Contains(year.Value.ToString()));
|
||||
|
||||
foreach (var title in synonyms)
|
||||
{
|
||||
var releaseTitle = groupName is "Movie" or "Live Action Movie" ?
|
||||
$"{releaseGroup}{title} {year} {infoString}" :
|
||||
$"{releaseGroup}{title} {releaseInfo} {infoString}";
|
||||
$"{releaseGroup}{title}{(useYearInTitle ? $" {year}" : string.Empty)} {releaseInfo} {infoString}";
|
||||
|
||||
var guid = new Uri(details + "?nh=" + HashUtil.CalculateMd5(title));
|
||||
|
||||
@@ -650,7 +669,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var advancedSeasonRegex = new Regex(@"\b(?:(?<season>\d+)(?:st|nd|rd|th) Season|Season (?<season>\d+))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
var seasonCharactersRegex = new Regex(@"(I{2,})$", RegexOptions.Compiled);
|
||||
var seasonNumberRegex = new Regex(@"\b(?<!Part[- ._])(?:S)?(?<season>[2-9])$", RegexOptions.Compiled);
|
||||
var seasonNumberRegex = new Regex(@"\b(?<!Part[- ._])(?<!\d[/])(?:S)?(?<season>[2-9])$", RegexOptions.Compiled);
|
||||
|
||||
foreach (var title in titles)
|
||||
{
|
||||
@@ -755,7 +774,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public int Matches { get; set; }
|
||||
|
||||
[JsonPropertyName("Groups")]
|
||||
public AnimeBytesGroup[] Groups { get; set; }
|
||||
public IReadOnlyCollection<AnimeBytesGroup> Groups { get; set; }
|
||||
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
public class AnimeBytesGroup
|
||||
@@ -783,16 +804,16 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public string Image { get; set; }
|
||||
|
||||
[JsonPropertyName("SynonymnsV2")]
|
||||
public Dictionary<string, string> Synonymns { get; set; }
|
||||
public IReadOnlyDictionary<string, string> Synonymns { get; set; }
|
||||
|
||||
[JsonPropertyName("Description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("Tags")]
|
||||
public List<string> Tags { get; set; }
|
||||
public IReadOnlyCollection<string> Tags { get; set; }
|
||||
|
||||
[JsonPropertyName("Torrents")]
|
||||
public List<AnimeBytesTorrent> Torrents { get; set; }
|
||||
public IReadOnlyCollection<AnimeBytesTorrent> Torrents { get; set; }
|
||||
}
|
||||
|
||||
public class AnimeBytesTorrent
|
||||
@@ -831,7 +852,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public int FileCount { get; set; }
|
||||
|
||||
[JsonPropertyName("FileList")]
|
||||
public List<AnimeBytesFile> Files { get; set; }
|
||||
public IReadOnlyCollection<AnimeBytesFile> Files { get; set; }
|
||||
|
||||
[JsonPropertyName("UploadTime")]
|
||||
public string UploadTime { get; set; }
|
||||
|
||||
@@ -119,6 +119,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.BooksComics, "Doujinshi");
|
||||
caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.BooksComics, "Doujinshi 18+");
|
||||
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.Audio, "OST");
|
||||
caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.AudioAudiobook, "Audiobooks");
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
@@ -326,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
Guid = $"BTN-{torrent.TorrentID}",
|
||||
InfoUrl = $"{protocol}//broadcasthe.net/torrents.php?id={torrent.GroupID}&torrentid={torrent.TorrentID}",
|
||||
DownloadUrl = RegexProtocol.Replace(torrent.DownloadURL, protocol),
|
||||
Title = CleanReleaseName(torrent.ReleaseName),
|
||||
Title = GetTitle(torrent),
|
||||
Categories = _categories.MapTrackerCatToNewznab(torrent.Resolution),
|
||||
InfoHash = torrent.InfoHash,
|
||||
Size = torrent.Size,
|
||||
@@ -136,9 +136,17 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
private string CleanReleaseName(string releaseName)
|
||||
private static string GetTitle(BroadcastheNetTorrent torrent)
|
||||
{
|
||||
return releaseName.Replace("\\", "");
|
||||
var releaseName = torrent.ReleaseName.Replace("\\", "");
|
||||
|
||||
if (torrent.Container.ToUpperInvariant() is "M2TS" or "ISO")
|
||||
{
|
||||
releaseName = Regex.Replace(releaseName, @"\b(H\.?265)\b", "HEVC", RegexOptions.Compiled);
|
||||
releaseName = Regex.Replace(releaseName, @"\b(H\.?264)\b", "AVC", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
return releaseName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -95,3 +95,8 @@ public class GazelleIndexResponse
|
||||
public string Authkey { get; set; }
|
||||
public string Passkey { get; set; }
|
||||
}
|
||||
|
||||
public class GazelleErrorResponse
|
||||
{
|
||||
public string Error { get; init; }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -32,7 +33,9 @@ public class GazelleParser : IParseIndexerResponse
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
STJson.TryDeserialize<GazelleErrorResponse>(indexerResponse.Content, out var errorResponse);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
|
||||
@@ -7,6 +7,7 @@ using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Definitions.Gazelle;
|
||||
@@ -148,7 +149,9 @@ public class GreatPosterWallParser : GazelleParser
|
||||
throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from indexer request");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
STJson.TryDeserialize<GazelleErrorResponse>(indexerResponse.Content, out var errorResponse);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
|
||||
@@ -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 + ")");
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return httpResponse.Content.Contains("You do not have permission to access this page.");
|
||||
return !httpResponse.Content.Contains("logout.php");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
|
||||
349
src/NzbDrone.Core/Indexers/Definitions/Knaben.cs
Normal file
349
src/NzbDrone.Core/Indexers/Definitions/Knaben.cs
Normal file
@@ -0,0 +1,349 @@
|
||||
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.org/" };
|
||||
public override string[] LegacyUrls => 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 ApiSearchEndpoint = "https://api.knaben.org/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(ApiSearchEndpoint, 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; }
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
@@ -385,11 +384,6 @@ public class MTeamTpParser : IParseIndexerResponse
|
||||
MinimumSeedTime = 172800 // 2 days
|
||||
};
|
||||
|
||||
if (torrent.Imdb.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
release.ImdbId = ParseUtil.GetImdbId(torrent.Imdb.TrimEnd('/').Split('/').LastOrDefault()).GetValueOrDefault();
|
||||
}
|
||||
|
||||
if (torrent.Status?.CreatedDate != null &&
|
||||
DateTime.TryParseExact($"{torrent.Status.CreatedDate} +08:00", "yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var publishDate))
|
||||
{
|
||||
|
||||
@@ -294,14 +294,21 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
parameters.Set("tor[srchIn][filenames]", "true");
|
||||
}
|
||||
|
||||
var catList = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
if (_settings.SearchLanguages.Any())
|
||||
{
|
||||
foreach (var (language, index) in _settings.SearchLanguages.Select((value, index) => (value, index)))
|
||||
{
|
||||
parameters.Set($"tor[browse_lang][{index}]", language.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
var catList = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList();
|
||||
|
||||
if (catList.Any())
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var cat in catList)
|
||||
foreach (var (category, index) in catList.Select((value, index) => (value, index)))
|
||||
{
|
||||
parameters.Set("tor[cat][" + index + "]", cat);
|
||||
index++;
|
||||
parameters.Set($"tor[cat][{index}]", category);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -441,6 +448,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return releaseInfos.ToArray();
|
||||
}
|
||||
|
||||
if (jsonResponse.Data == null)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Unexpected response content from indexer request: {0}", jsonResponse.Message ?? "Check the logs for more information.");
|
||||
}
|
||||
|
||||
var hasUserVip = HasUserVip(httpResponse.GetCookies());
|
||||
|
||||
foreach (var item in jsonResponse.Data)
|
||||
@@ -579,6 +591,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
SearchInDescription = false;
|
||||
SearchInSeries = false;
|
||||
SearchInFilenames = false;
|
||||
SearchLanguages = Array.Empty<int>();
|
||||
}
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Textbox, Label = "Mam Id", HelpText = "Mam Session Id (Created Under Preferences -> Security)")]
|
||||
@@ -599,6 +612,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
[FieldDefinition(7, Type = FieldType.Checkbox, Label = "Search in filenames", HelpText = "Search text in the filenames")]
|
||||
public bool SearchInFilenames { get; set; }
|
||||
|
||||
[FieldDefinition(8, Type = FieldType.Select, Label = "Search Languages", SelectOptions = typeof(MyAnonamouseSearchLanguages), HelpText = "Specify the desired languages. If unspecified, all options are used.")]
|
||||
public IEnumerable<int> SearchLanguages { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
@@ -626,6 +642,198 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
NotVip = 5,
|
||||
}
|
||||
|
||||
public enum MyAnonamouseSearchLanguages
|
||||
{
|
||||
[FieldOption(Label="English")]
|
||||
English = 1,
|
||||
|
||||
[FieldOption(Label="Afrikaans")]
|
||||
Afrikaans = 17,
|
||||
|
||||
[FieldOption(Label="Arabic")]
|
||||
Arabic = 32,
|
||||
|
||||
[FieldOption(Label="Bengali")]
|
||||
Bengali = 35,
|
||||
|
||||
[FieldOption(Label="Bosnian")]
|
||||
Bosnian = 51,
|
||||
|
||||
[FieldOption(Label="Bulgarian")]
|
||||
Bulgarian = 18,
|
||||
|
||||
[FieldOption(Label="Burmese")]
|
||||
Burmese = 6,
|
||||
|
||||
[FieldOption(Label="Cantonese")]
|
||||
Cantonese = 44,
|
||||
|
||||
[FieldOption(Label="Catalan")]
|
||||
Catalan = 19,
|
||||
|
||||
[FieldOption(Label="Chinese")]
|
||||
Chinese = 2,
|
||||
|
||||
[FieldOption(Label="Croatian")]
|
||||
Croatian = 49,
|
||||
|
||||
[FieldOption(Label="Czech")]
|
||||
Czech = 20,
|
||||
|
||||
[FieldOption(Label="Danish")]
|
||||
Danish = 21,
|
||||
|
||||
[FieldOption(Label="Dutch")]
|
||||
Dutch = 22,
|
||||
|
||||
[FieldOption(Label="Estonian")]
|
||||
Estonian = 61,
|
||||
|
||||
[FieldOption(Label="Farsi")]
|
||||
Farsi = 39,
|
||||
|
||||
[FieldOption(Label="Finnish")]
|
||||
Finnish = 23,
|
||||
|
||||
[FieldOption(Label="French")]
|
||||
French = 36,
|
||||
|
||||
[FieldOption(Label="German")]
|
||||
German = 37,
|
||||
|
||||
[FieldOption(Label="Greek")]
|
||||
Greek = 26,
|
||||
|
||||
[FieldOption(Label="Greek, Ancient")]
|
||||
GreekAncient = 59,
|
||||
|
||||
[FieldOption(Label="Gujarati")]
|
||||
Gujarati = 3,
|
||||
|
||||
[FieldOption(Label="Hebrew")]
|
||||
Hebrew = 27,
|
||||
|
||||
[FieldOption(Label="Hindi")]
|
||||
Hindi = 8,
|
||||
|
||||
[FieldOption(Label="Hungarian")]
|
||||
Hungarian = 28,
|
||||
|
||||
[FieldOption(Label="Icelandic")]
|
||||
Icelandic = 63,
|
||||
|
||||
[FieldOption(Label="Indonesian")]
|
||||
Indonesian = 53,
|
||||
|
||||
[FieldOption(Label="Irish")]
|
||||
Irish = 56,
|
||||
|
||||
[FieldOption(Label="Italian")]
|
||||
Italian = 43,
|
||||
|
||||
[FieldOption(Label="Japanese")]
|
||||
Japanese = 38,
|
||||
|
||||
[FieldOption(Label="Javanese")]
|
||||
Javanese = 12,
|
||||
|
||||
[FieldOption(Label="Kannada")]
|
||||
Kannada = 5,
|
||||
|
||||
[FieldOption(Label="Korean")]
|
||||
Korean = 41,
|
||||
|
||||
[FieldOption(Label="Lithuanian")]
|
||||
Lithuanian = 50,
|
||||
|
||||
[FieldOption(Label="Latin")]
|
||||
Latin = 46,
|
||||
|
||||
[FieldOption(Label="Latvian")]
|
||||
Latvian = 62,
|
||||
|
||||
[FieldOption(Label="Malay")]
|
||||
Malay = 33,
|
||||
|
||||
[FieldOption(Label="Malayalam")]
|
||||
Malayalam = 58,
|
||||
|
||||
[FieldOption(Label="Manx")]
|
||||
Manx = 57,
|
||||
|
||||
[FieldOption(Label="Marathi")]
|
||||
Marathi = 9,
|
||||
|
||||
[FieldOption(Label="Norwegian")]
|
||||
Norwegian = 48,
|
||||
|
||||
[FieldOption(Label="Polish")]
|
||||
Polish = 45,
|
||||
|
||||
[FieldOption(Label="Portuguese")]
|
||||
Portuguese = 34,
|
||||
|
||||
[FieldOption(Label="Brazilian Portuguese (BP)")]
|
||||
BrazilianPortuguese = 52,
|
||||
|
||||
[FieldOption(Label="Punjabi")]
|
||||
Punjabi = 14,
|
||||
|
||||
[FieldOption(Label="Romanian")]
|
||||
Romanian = 30,
|
||||
|
||||
[FieldOption(Label="Russian")]
|
||||
Russian = 16,
|
||||
|
||||
[FieldOption(Label="Scottish Gaelic")]
|
||||
ScottishGaelic = 24,
|
||||
|
||||
[FieldOption(Label="Sanskrit")]
|
||||
Sanskrit = 60,
|
||||
|
||||
[FieldOption(Label="Serbian")]
|
||||
Serbian = 31,
|
||||
|
||||
[FieldOption(Label="Slovenian")]
|
||||
Slovenian = 54,
|
||||
|
||||
[FieldOption(Label="Spanish")]
|
||||
Spanish = 4,
|
||||
|
||||
[FieldOption(Label="Castilian Spanish")]
|
||||
CastilianSpanish = 55,
|
||||
|
||||
[FieldOption(Label="Swedish")]
|
||||
Swedish = 40,
|
||||
|
||||
[FieldOption(Label="Tagalog")]
|
||||
Tagalog = 29,
|
||||
|
||||
[FieldOption(Label="Tamil")]
|
||||
Tamil = 11,
|
||||
|
||||
[FieldOption(Label="Telugu")]
|
||||
Telugu = 10,
|
||||
|
||||
[FieldOption(Label="Thai")]
|
||||
Thai = 7,
|
||||
|
||||
[FieldOption(Label="Turkish")]
|
||||
Turkish = 42,
|
||||
|
||||
[FieldOption(Label="Ukrainian")]
|
||||
Ukrainian = 25,
|
||||
|
||||
[FieldOption(Label="Urdu")]
|
||||
Urdu = 15,
|
||||
|
||||
[FieldOption(Label="Vietnamese")]
|
||||
Vietnamese = 13,
|
||||
|
||||
[FieldOption(Label="Other")]
|
||||
Other = 47,
|
||||
}
|
||||
|
||||
public class MyAnonamouseTorrent
|
||||
{
|
||||
public int Id { get; set; }
|
||||
@@ -655,7 +863,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class MyAnonamouseResponse
|
||||
{
|
||||
public string Error { get; set; }
|
||||
public List<MyAnonamouseTorrent> Data { get; set; }
|
||||
public IReadOnlyCollection<MyAnonamouseTorrent> Data { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public class MyAnonamouseBuyPersonalFreeleechResponse
|
||||
|
||||
@@ -322,7 +322,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
ApiKey = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(4, Label = "ApiKey", HelpText = "IndexerNebulanceSettingsApiKeyHelpText")]
|
||||
[FieldDefinition(2, Label = "ApiKey", HelpText = "IndexerNebulanceSettingsApiKeyHelpText", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
|
||||
RuleFor(c => c.VipExpiration).Must(c => c.IsFutureDate())
|
||||
.When(c => c.VipExpiration.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Must be a future date");
|
||||
.WithMessage("Must be a future date")
|
||||
.AsWarning();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ public class NorBitsRequestGenerator : IIndexerRequestGenerator
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
searchTerm = "search=" + term.UrlEncode(Encoding.GetEncoding(28591));
|
||||
searchTerm = "search=" + term.UrlEncode(Encoding.UTF8);
|
||||
}
|
||||
|
||||
searchUrl += "?" + searchTerm + "&" + parameters.GetQueryString();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -8,6 +8,7 @@ using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Definitions.Gazelle;
|
||||
@@ -252,7 +253,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
STJson.TryDeserialize<GazelleErrorResponse>(indexerResponse.Content, out var errorResponse);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
@@ -15,6 +16,7 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
public override bool SupportsSearch => true;
|
||||
public override bool SupportsPagination => true;
|
||||
public override int PageSize => 50;
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);
|
||||
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
|
||||
@@ -56,6 +56,19 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
{
|
||||
foreach (var torrent in result.Torrents)
|
||||
{
|
||||
// skip non-freeleech results when freeleech only is set
|
||||
var downloadVolumeFactor = torrent.FreeleechType?.ToUpperInvariant() switch
|
||||
{
|
||||
"FREELEECH" => 0,
|
||||
"HALF LEECH" => 0.5,
|
||||
_ => 1
|
||||
};
|
||||
|
||||
if (_settings.FreeleechOnly && downloadVolumeFactor != 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = torrent.Id;
|
||||
var title = torrent.ReleaseName;
|
||||
|
||||
@@ -94,12 +107,7 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn
|
||||
ImdbId = result.ImdbId.IsNotNullOrWhiteSpace() ? int.Parse(result.ImdbId) : 0,
|
||||
Scene = torrent.Scene,
|
||||
IndexerFlags = flags,
|
||||
DownloadVolumeFactor = torrent.FreeleechType?.ToUpperInvariant() switch
|
||||
{
|
||||
"FREELEECH" => 0,
|
||||
"HALF LEECH" => 0.5,
|
||||
_ => 1
|
||||
},
|
||||
DownloadVolumeFactor = downloadVolumeFactor,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 345600,
|
||||
|
||||
@@ -8,6 +8,7 @@ using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Definitions.Gazelle;
|
||||
@@ -24,7 +25,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class Redacted : TorrentIndexerBase<RedactedSettings>
|
||||
{
|
||||
public override string Name => "Redacted";
|
||||
public override string[] IndexerUrls => new[] { "https://redacted.ch/" };
|
||||
public override string[] IndexerUrls => new[] { "https://redacted.sh/" };
|
||||
public override string[] LegacyUrls => new[] { "https://redacted.ch/" };
|
||||
public override string Description => "REDActed (Aka.PassTheHeadPhones) is one of the most well-known music trackers.";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
@@ -251,7 +253,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
STJson.TryDeserialize<GazelleErrorResponse>(indexerResponse.Content, out var errorResponse);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
|
||||
@@ -933,6 +933,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(1224, NewznabStandardCategory.AudioLossless, "|- Авторская песня (lossless)");
|
||||
caps.Categories.AddCategoryMapping(1225, NewznabStandardCategory.AudioMP3, "|- Авторская песня (lossy)");
|
||||
caps.Categories.AddCategoryMapping(1226, NewznabStandardCategory.Audio, "|- Менестрели и ролевики (lossy и lossless)");
|
||||
caps.Categories.AddCategoryMapping(782, NewznabStandardCategory.Audio, "Лейбл- и сцен-паки. Неофициальные сборники и ремастеринги. AI-музыка");
|
||||
caps.Categories.AddCategoryMapping(577, NewznabStandardCategory.Audio, "|- AI-Music - музыка ИИ, нейросетей (lossy и lossless)");
|
||||
caps.Categories.AddCategoryMapping(1842, NewznabStandardCategory.AudioLossless, "Label Packs (lossless)");
|
||||
caps.Categories.AddCategoryMapping(1648, NewznabStandardCategory.AudioMP3, "Label packs, Scene packs (lossy)");
|
||||
caps.Categories.AddCategoryMapping(134, NewznabStandardCategory.AudioLossless, "|- Неофициальные сборники и ремастеринги (lossless)");
|
||||
@@ -1292,7 +1294,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(650, NewznabStandardCategory.PCMobileOther, "Игры для мобильных устройств");
|
||||
caps.Categories.AddCategoryMapping(2149, NewznabStandardCategory.PCMobileAndroid, "|- Игры для Android");
|
||||
caps.Categories.AddCategoryMapping(2420, NewznabStandardCategory.ConsoleOther, "|- Игры для Oculus Quest");
|
||||
caps.Categories.AddCategoryMapping(1001, NewznabStandardCategory.PC, "|- Игры для Java");
|
||||
caps.Categories.AddCategoryMapping(1004, NewznabStandardCategory.PCMobileOther, "|- Игры для Symbian");
|
||||
caps.Categories.AddCategoryMapping(1002, NewznabStandardCategory.PCMobileOther, "|- Игры для Windows Mobile");
|
||||
caps.Categories.AddCategoryMapping(240, NewznabStandardCategory.OtherMisc, "Игровое видео");
|
||||
@@ -1308,7 +1309,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(1379, NewznabStandardCategory.PC, "|- Операционные системы (Linux, Unix)");
|
||||
caps.Categories.AddCategoryMapping(1381, NewznabStandardCategory.PC, "|- Программное обеспечение (Linux, Unix)");
|
||||
caps.Categories.AddCategoryMapping(1473, NewznabStandardCategory.PC, "|- Другие ОС и ПО под них");
|
||||
caps.Categories.AddCategoryMapping(1195, NewznabStandardCategory.PC, "Тестовые диски для настройки аудио/видео аппаратуры");
|
||||
caps.Categories.AddCategoryMapping(1013, NewznabStandardCategory.PC, "Системные программы");
|
||||
caps.Categories.AddCategoryMapping(1028, NewznabStandardCategory.PC, "|- Работа с жёстким диском");
|
||||
caps.Categories.AddCategoryMapping(1029, NewznabStandardCategory.PC, "|- Резервное копирование");
|
||||
@@ -1350,6 +1350,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(1018, NewznabStandardCategory.PC, "|- Шаблоны для сайтов и CMS");
|
||||
caps.Categories.AddCategoryMapping(1058, NewznabStandardCategory.PC, "|- Разное (Веб-разработка и программирование)");
|
||||
caps.Categories.AddCategoryMapping(1016, NewznabStandardCategory.PC, "Программы для работы с мультимедиа и 3D");
|
||||
caps.Categories.AddCategoryMapping(1195, NewznabStandardCategory.PC, "|- Тестовые диски для настройки аудио/видео аппаратуры");
|
||||
caps.Categories.AddCategoryMapping(1079, NewznabStandardCategory.PC, "|- Программные комплекты");
|
||||
caps.Categories.AddCategoryMapping(1080, NewznabStandardCategory.PC, "|- Плагины для программ компании Adobe");
|
||||
caps.Categories.AddCategoryMapping(1081, NewznabStandardCategory.PC, "|- Графические редакторы");
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.Definitions.Gazelle;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
@@ -78,7 +79,9 @@ public class SecretCinemaParser : IParseIndexerResponse
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request");
|
||||
STJson.TryDeserialize<GazelleErrorResponse>(indexerResponse.Content, out var errorResponse);
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
|
||||
@@ -7,6 +7,7 @@ using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -52,7 +53,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new TorrentDayParser(Settings, Capabilities.Categories);
|
||||
return new TorrentDayParser(Settings, Capabilities.Categories, _logger);
|
||||
}
|
||||
|
||||
protected override IDictionary<string, string> GetCookies()
|
||||
@@ -228,15 +229,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly TorrentDaySettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public TorrentDayParser(TorrentDaySettings settings, IndexerCapabilitiesCategories categories)
|
||||
public TorrentDayParser(TorrentDaySettings settings, IndexerCapabilitiesCategories categories, Logger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
if (indexerResponse.HttpResponse.HasHttpRedirect)
|
||||
{
|
||||
_logger.Warn("Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl);
|
||||
|
||||
if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("/login.php"))
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie and try testing the indexer.");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, "Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl);
|
||||
}
|
||||
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
|
||||
var rows = JsonConvert.DeserializeObject<dynamic>(indexerResponse.Content);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -44,6 +46,13 @@ namespace NzbDrone.Core.Indexers.Definitions.TorrentRss
|
||||
}
|
||||
}
|
||||
|
||||
protected override Task<ValidationFailure> TestConnection()
|
||||
{
|
||||
UpdateCookies(null, null);
|
||||
|
||||
return base.TestConnection();
|
||||
}
|
||||
|
||||
private IndexerDefinition GetDefinition(string name, string description, TorrentRssIndexerSettings settings)
|
||||
{
|
||||
return new IndexerDefinition
|
||||
|
||||
@@ -121,8 +121,15 @@ public class XSpeeds : TorrentIndexerBase<XSpeedsSettings>
|
||||
caps.Categories.AddCategoryMapping(112, NewznabStandardCategory.MoviesOther, "Anime Movies");
|
||||
caps.Categories.AddCategoryMapping(111, NewznabStandardCategory.MoviesOther, "Anime TV");
|
||||
caps.Categories.AddCategoryMapping(150, NewznabStandardCategory.PC, "Apps");
|
||||
caps.Categories.AddCategoryMapping(80, NewznabStandardCategory.AudioAudiobook, "Audiobooks");
|
||||
caps.Categories.AddCategoryMapping(48, NewznabStandardCategory.Books, "Books Magazines");
|
||||
caps.Categories.AddCategoryMapping(156, NewznabStandardCategory.TV, "AV1");
|
||||
caps.Categories.AddCategoryMapping(156, NewznabStandardCategory.Movies, "AV1");
|
||||
caps.Categories.AddCategoryMapping(159, NewznabStandardCategory.Movies, "Movie Boxsets AV1");
|
||||
caps.Categories.AddCategoryMapping(158, NewznabStandardCategory.Movies, "Movies AV1");
|
||||
caps.Categories.AddCategoryMapping(157, NewznabStandardCategory.TV, "TV AV1");
|
||||
caps.Categories.AddCategoryMapping(160, NewznabStandardCategory.TV, "TV Boxsets AV1");
|
||||
caps.Categories.AddCategoryMapping(153, NewznabStandardCategory.Books, "Books");
|
||||
caps.Categories.AddCategoryMapping(154, NewznabStandardCategory.AudioAudiobook, "Audiobooks");
|
||||
caps.Categories.AddCategoryMapping(155, NewznabStandardCategory.Books, "Books & Magazines");
|
||||
caps.Categories.AddCategoryMapping(68, NewznabStandardCategory.MoviesOther, "Cams/TS");
|
||||
caps.Categories.AddCategoryMapping(140, NewznabStandardCategory.TVDocumentary, "Documentary");
|
||||
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.MoviesDVD, "DVDR");
|
||||
@@ -154,6 +161,7 @@ public class XSpeeds : TorrentIndexerBase<XSpeedsSettings>
|
||||
caps.Categories.AddCategoryMapping(146, NewznabStandardCategory.MoviesSD, "Movies SD");
|
||||
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.Audio, "Music");
|
||||
caps.Categories.AddCategoryMapping(135, NewznabStandardCategory.AudioLossless, "Music/FLAC");
|
||||
caps.Categories.AddCategoryMapping(151, NewznabStandardCategory.Audio, "Karaoke");
|
||||
caps.Categories.AddCategoryMapping(136, NewznabStandardCategory.Audio, "Music Boxset");
|
||||
caps.Categories.AddCategoryMapping(148, NewznabStandardCategory.AudioVideo, "Music Videos");
|
||||
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.Other, "Other");
|
||||
|
||||
@@ -650,7 +650,7 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
foreach (var cookie in Cookies)
|
||||
{
|
||||
request.HttpRequest.Cookies.Add(cookie.Key, cookie.Value);
|
||||
request.HttpRequest.Cookies[cookie.Key] = cookie.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,7 +761,7 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
if (releases.Releases.Empty())
|
||||
{
|
||||
return new ValidationFailure(string.Empty, "Query successful, but no results were returned from your indexer. This may be an issue with the indexer, your indexer category settings, or other indexer settings such as search freeleech only etc.");
|
||||
return new ValidationFailure(string.Empty, "Query successful, but no results were returned from your indexer. This may be an issue with the indexer, your indexer category settings, or other indexer settings such as search freeleech only etc. See the FAQ for details.");
|
||||
}
|
||||
}
|
||||
catch (IndexerAuthException ex)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user