mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
Compare commits
46 Commits
phantom-fo
...
phantom-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8502f523e6 | ||
|
|
a7ca139e13 | ||
|
|
94a78eabe5 | ||
|
|
0c7743e786 | ||
|
|
d0f0fc787e | ||
|
|
ce3c151b8c | ||
|
|
f49d2338fd | ||
|
|
164f46e4c0 | ||
|
|
9f527718f2 | ||
|
|
ee32829cdb | ||
|
|
43cb44dd38 | ||
|
|
e8854a2675 | ||
|
|
b4c27f5d34 | ||
|
|
9dab2ba6e4 | ||
|
|
d105dd47e0 | ||
|
|
f4f2a6f5fc | ||
|
|
3542ab86e9 | ||
|
|
f76babc699 | ||
|
|
da7fec4d35 | ||
|
|
488e7b1a26 | ||
|
|
f45b27f507 | ||
|
|
796c5e8b6b | ||
|
|
89a4249072 | ||
|
|
05ffdd07a2 | ||
|
|
14fee1c086 | ||
|
|
3a7b0cacb8 | ||
|
|
ddd70fd198 | ||
|
|
fc231c5ef8 | ||
|
|
933832fe2c | ||
|
|
4ed1f6b814 | ||
|
|
9ed1d27f86 | ||
|
|
4057a3112d | ||
|
|
0b01b75cac | ||
|
|
9c635781bd | ||
|
|
b6bfeaaba3 | ||
|
|
0318a4a5e1 | ||
|
|
2d985c0c6a | ||
|
|
3ef47b0ce3 | ||
|
|
213db3b107 | ||
|
|
d475ee37c3 | ||
|
|
1d9ed1b56d | ||
|
|
e34b6a36d5 | ||
|
|
8468a74ade | ||
|
|
34cbdee510 | ||
|
|
924f6ca715 | ||
|
|
9eb24cedd6 |
12
FUNDING.yml
Normal file
12
FUNDING.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: sonarr
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
33
Logo/Jetbrains/dottrace.svg
Normal file
33
Logo/Jetbrains/dottrace.svg
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
66
Logo/Jetbrains/jetbrains.svg
Normal file
66
Logo/Jetbrains/jetbrains.svg
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
50
Logo/Jetbrains/resharper.svg
Normal file
50
Logo/Jetbrains/resharper.svg
Normal file
@@ -0,0 +1,50 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
64
Logo/Jetbrains/teamcity.svg
Normal file
64
Logo/Jetbrains/teamcity.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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" 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.7738" y1="31.2729" x2="40.1662" y2="31.2729">
|
||||
<stop offset="0" style="stop-color:#905CFB"/>
|
||||
<stop offset="6.772543e-002" style="stop-color:#776CF9"/>
|
||||
<stop offset="0.1729" style="stop-color:#5681F7"/>
|
||||
<stop offset="0.2865" style="stop-color:#3B92F5"/>
|
||||
<stop offset="0.4097" style="stop-color:#269FF4"/>
|
||||
<stop offset="0.5474" style="stop-color:#17A9F3"/>
|
||||
<stop offset="0.7111" style="stop-color:#0FAEF2"/>
|
||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_1_);" d="M39.7,47.9l-6.1-34c-0.4-2.4-1.2-4.8-2.7-7.1c-2-3.2-5.2-5.4-8.8-6.3
|
||||
C7.9-2.9-2.6,11.3,3.6,23.9c0,0,0,0,0,0l14.8,31.7c0.4,1,1,2,1.7,2.9c1.2,1.6,2.8,2.8,4.7,3.4C34.4,64.9,42.1,56.4,39.7,47.9z"/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="5.3113" y1="9.6691" x2="69.2278" y2="43.8664">
|
||||
<stop offset="0" style="stop-color:#905CFB"/>
|
||||
<stop offset="6.772543e-002" style="stop-color:#776CF9"/>
|
||||
<stop offset="0.1729" style="stop-color:#5681F7"/>
|
||||
<stop offset="0.2865" style="stop-color:#3B92F5"/>
|
||||
<stop offset="0.4097" style="stop-color:#269FF4"/>
|
||||
<stop offset="0.5474" style="stop-color:#17A9F3"/>
|
||||
<stop offset="0.7111" style="stop-color:#0FAEF2"/>
|
||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_2_);" d="M67.4,26.5c-1.4-2.2-3.4-3.9-5.7-4.9L25.5,1.7l0,0c-1-0.5-2.1-1-3.3-1.3
|
||||
C6.7-3.2-4.4,13.8,5.5,27c1.5,2,3.6,3.6,6,4.5L48,47.9c0.8,0.5,1.6,0.8,2.5,1.1C64.5,53.4,75.1,38.6,67.4,26.5z"/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-19.2836" y1="70.8198" x2="55.9833" y2="33.1863">
|
||||
<stop offset="0" style="stop-color:#3BEA62"/>
|
||||
<stop offset="0.117" style="stop-color:#31DE80"/>
|
||||
<stop offset="0.3025" style="stop-color:#24CEA8"/>
|
||||
<stop offset="0.4844" style="stop-color:#1AC1C9"/>
|
||||
<stop offset="0.6592" style="stop-color:#12B7DF"/>
|
||||
<stop offset="0.8238" style="stop-color:#0EB2ED"/>
|
||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_3_);" d="M67.4,26.5c-1.8-2.8-4.6-4.8-7.9-5.6c-3.5-0.8-6.8-0.5-9.6,0.7L11.4,36.1
|
||||
c0,0-0.2,0.1-0.6,0.4C0.9,40.4-4,53.3,4,64c1.8,2.4,4.3,4.2,7.1,5c5.3,1.6,10.1,1,14-1.1c0,0,0.1,0,0.1,0l37.6-20.1
|
||||
c0,0,0,0,0.1-0.1C69.5,43.9,72.6,34.6,67.4,26.5z"/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="38.9439" y1="5.8503" x2="5.4232" y2="77.5093">
|
||||
<stop offset="0" style="stop-color:#3BEA62"/>
|
||||
<stop offset="9.397750e-002" style="stop-color:#2FDB87"/>
|
||||
<stop offset="0.196" style="stop-color:#24CEA8"/>
|
||||
<stop offset="0.3063" style="stop-color:#1BC3C3"/>
|
||||
<stop offset="0.4259" style="stop-color:#14BAD8"/>
|
||||
<stop offset="0.5596" style="stop-color:#10B5E7"/>
|
||||
<stop offset="0.7185" style="stop-color:#0DB1EF"/>
|
||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_4_);" d="M50.3,12.8c1.2-2.7,1.1-6-0.9-9c-1.1-1.8-2.9-3-4.9-3.5c-4.5-1.1-8.3,1-10.1,4.2L3.5,42
|
||||
c0,0,0,0,0,0.1C-0.9,47.9-1.6,56.5,4,64c1.8,2.4,4.3,4.2,7.1,5c10.5,3.3,19.3-2.5,22.1-10.8L50.3,12.8z"/>
|
||||
</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"/>
|
||||
<polygon style="fill:#FFFFFF;" points="22.9,22.7 17.5,22.7 17.5,19.1 32.3,19.1 32.3,22.7 26.8,22.7 26.8,37 22.9,37 "/>
|
||||
<path style="fill:#FFFFFF;" d="M32.5,28.1L32.5,28.1c0-5.1,3.8-9.3,9.3-9.3c3.4,0,5.4,1.1,7.1,2.8l-2.5,2.9c-1.4-1.3-2.8-2-4.6-2
|
||||
c-3,0-5.2,2.5-5.2,5.6V28c0,3.1,2.1,5.6,5.2,5.6c2,0,3.3-0.8,4.7-2.1l2.5,2.5c-1.8,2-3.9,3.2-7.3,3.2
|
||||
C36.4,37.3,32.5,33.2,32.5,28.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
27
README.md
27
README.md
@@ -66,8 +66,27 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
|
||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
- Copyright 2010-2020
|
||||
|
||||
### Sponsors
|
||||
### Supporters
|
||||
|
||||
This project would not be possible without the support of our users and software providers. [**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
|
||||
|
||||
#### Sponsors
|
||||
|
||||
[](https://opencollective.com/sonarr/contribute/sponsor-21443/checkout)
|
||||
|
||||
#### Flexible Sponsors
|
||||
|
||||
[](https://opencollective.com/sonarr/contribute/flexible-sponsor-21457/checkout)
|
||||
|
||||
#### Backers
|
||||
|
||||
[](https://opencollective.com/sonarr/contribute/backer-21442/checkout)
|
||||
|
||||
#### JetBrains
|
||||
|
||||
Thank you to [<img src="/Logo/Jetbrains/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
||||
|
||||
* [<img src="/Logo/Jetbrains/teamcity.svg" alt="TeamCity" width="32"> TeamCity](http://www.jetbrains.com/teamcity/)
|
||||
* [<img src="/Logo/Jetbrains/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||
* [<img src="/Logo/Jetbrains/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||
|
||||
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
||||
- [ReSharper](http://www.jetbrains.com/resharper/)
|
||||
- [TeamCity](http://www.jetbrains.com/teamcity/)
|
||||
|
||||
@@ -43,7 +43,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}"
|
||||
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive
|
||||
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
|
||||
@@ -75,7 +75,8 @@ function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Exec(ExpandConstant('{commonappdata}\NzbDrone\bin\NzbDrone.Console.exe'), '/u', '', 0, ewWaitUntilTerminated, ResultCode)
|
||||
Exec('net', 'stop nzbdrone', '', 0, ewWaitUntilTerminated, ResultCode)
|
||||
Exec('sc', 'delete nzbdrone', '', 0, ewWaitUntilTerminated, ResultCode)
|
||||
end;
|
||||
|
||||
function Framework472IsNotInstalled(): Boolean;
|
||||
|
||||
@@ -6,6 +6,7 @@ const webpack = require('webpack');
|
||||
const errorHandler = require('./helpers/errorHandler');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackPluginHtmlTags = require('html-webpack-plugin/lib/html-tags');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
const uiFolder = 'UI';
|
||||
@@ -13,7 +14,7 @@ const frontendFolder = path.join(__dirname, '..');
|
||||
const srcFolder = path.join(frontendFolder, 'src');
|
||||
const isProduction = process.argv.indexOf('--production') > -1;
|
||||
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
|
||||
const inlineWebWorkers = true;
|
||||
const inlineWebWorkers = 'no-fallback';
|
||||
|
||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||
|
||||
@@ -31,14 +32,19 @@ const cssVarsFiles = [
|
||||
].map(require.resolve);
|
||||
|
||||
// Override the way HtmlWebpackPlugin injects the scripts
|
||||
// TODO: Find a better way to get these paths without
|
||||
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
|
||||
const head = assetTags.head.map((v) => {
|
||||
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` };
|
||||
return this.createHtmlTag(v);
|
||||
const head = assetTags.headTags.map((v) => {
|
||||
const href = v.attributes.href
|
||||
.replace('\\', '/')
|
||||
.replace('%5C', '/');
|
||||
|
||||
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${href}` };
|
||||
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||
});
|
||||
const body = assetTags.body.map((v) => {
|
||||
const body = assetTags.bodyTags.map((v) => {
|
||||
v.attributes = { src: `/${v.attributes.src}` };
|
||||
return this.createHtmlTag(v);
|
||||
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||
});
|
||||
|
||||
return html
|
||||
@@ -122,9 +128,8 @@ const config = {
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
name: '[name].js',
|
||||
inline: inlineWebWorkers,
|
||||
fallback: !inlineWebWorkers
|
||||
filename: '[name].js',
|
||||
inline: inlineWebWorkers
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -264,7 +264,7 @@ class AddNewSeriesModalContent extends Component {
|
||||
<div>
|
||||
<label className={styles.searchLabelContainer}>
|
||||
<span className={styles.searchLabel}>
|
||||
Start search for missing episodes
|
||||
Start search for missing episodes
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
@@ -278,7 +278,7 @@ class AddNewSeriesModalContent extends Component {
|
||||
|
||||
<label className={styles.searchLabelContainer}>
|
||||
<span className={styles.searchLabel}>
|
||||
Start search for cutoff unmet episodes
|
||||
Start search for cutoff unmet episodes
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
|
||||
@@ -105,7 +105,7 @@ class AddNewSeriesSearchResult extends Component {
|
||||
{
|
||||
!title.contains(year) && year ?
|
||||
<span className={styles.year}>
|
||||
({year})
|
||||
({year})
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
@@ -168,7 +168,7 @@ class AddNewSeriesSearchResult extends Component {
|
||||
kind={kinds.DANGER}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Ended
|
||||
Ended
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
@@ -179,7 +179,7 @@ class AddNewSeriesSearchResult extends Component {
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Upcoming
|
||||
Upcoming
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ class ImportSeriesSelectSeries extends Component {
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
No match found!
|
||||
No match found!
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
@@ -189,7 +189,7 @@ class ImportSeriesSelectSeries extends Component {
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
Search failed, please try again later.
|
||||
Search failed, please try again later.
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ function CustomFiltersModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Custom Filters
|
||||
Custom Filters
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -58,7 +58,7 @@ function CustomFiltersModalContent(props) {
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions';
|
||||
import { fetchOptions, clearOptions, defaultState } from 'Store/Actions/providerOptionActions';
|
||||
import DeviceInput from './DeviceInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(state) => state.providerOptions,
|
||||
(state) => state.providerOptions.devices || defaultState,
|
||||
(value, devices) => {
|
||||
|
||||
return {
|
||||
@@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this.props.dispatchClearOptions();
|
||||
this.props.dispatchClearOptions({ section: 'devices' });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -65,6 +65,7 @@ class DeviceInputConnector extends Component {
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchOptions({
|
||||
section: 'devices',
|
||||
action: 'getDevices',
|
||||
provider,
|
||||
providerData
|
||||
|
||||
@@ -66,3 +66,8 @@
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
margin: 5px -5px 5px 0;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { icons, sizes, scrollDirections } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Portal from 'Components/Portal';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Measure from 'Components/Measure';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
@@ -250,6 +251,10 @@ class EnhancedSelectInput extends Component {
|
||||
this._addListener();
|
||||
}
|
||||
|
||||
if (!this.state.isOpen && this.props.onOpen) {
|
||||
this.props.onOpen();
|
||||
}
|
||||
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
|
||||
@@ -295,6 +300,7 @@ class EnhancedSelectInput extends Component {
|
||||
value,
|
||||
values,
|
||||
isDisabled,
|
||||
isFetching,
|
||||
hasError,
|
||||
hasWarning,
|
||||
valueOptions,
|
||||
@@ -355,9 +361,21 @@ class EnhancedSelectInput extends Component {
|
||||
styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
</Measure>
|
||||
@@ -483,12 +501,14 @@ EnhancedSelectInput.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
valueOptions: PropTypes.object.isRequired,
|
||||
selectedValueOptions: PropTypes.object.isRequired,
|
||||
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
optionComponent: PropTypes.elementType,
|
||||
onOpen: PropTypes.func,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -496,6 +516,7 @@ EnhancedSelectInput.defaultProps = {
|
||||
className: styles.enhancedSelect,
|
||||
disabledClassName: styles.isDisabled,
|
||||
isDisabled: false,
|
||||
isFetching: false,
|
||||
valueOptions: {},
|
||||
selectedValueOptions: {},
|
||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||
|
||||
159
frontend/src/Components/Form/EnhancedSelectInputConnector.js
Normal file
159
frontend/src/Components/Form/EnhancedSelectInputConnector.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchOptions, clearOptions, defaultState } from 'Store/Actions/providerOptionActions';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const importantFieldNames = [
|
||||
'baseUrl',
|
||||
'apiPath',
|
||||
'apiKey'
|
||||
];
|
||||
|
||||
function getProviderDataKey(providerData) {
|
||||
if (!providerData || !providerData.fields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fields = providerData.fields
|
||||
.filter((f) => importantFieldNames.includes(f.name))
|
||||
.map((f) => f.value);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function getSelectOptions(items) {
|
||||
if (!items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return items.map((option) => {
|
||||
return {
|
||||
key: option.value,
|
||||
value: option.name,
|
||||
hint: option.hint,
|
||||
parentKey: option.parentValue
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { selectOptionsProviderAction }) => state.providerOptions[selectOptionsProviderAction] || defaultState,
|
||||
(options) => {
|
||||
if (options) {
|
||||
return {
|
||||
isFetching: options.isFetching,
|
||||
values: getSelectOptions(options.items)
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchOptions: fetchOptions,
|
||||
dispatchClearOptions: clearOptions
|
||||
};
|
||||
|
||||
class EnhancedSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
refetchRequired: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this._populate();
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
const prevKey = getProviderDataKey(prevProps.providerData);
|
||||
const nextKey = getProviderDataKey(this.props.providerData);
|
||||
|
||||
if (!_.isEqual(prevKey, nextKey)) {
|
||||
this.setState({ refetchRequired: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOpen = () => {
|
||||
if (this.state.refetchRequired) {
|
||||
this._populate();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_populate() {
|
||||
const {
|
||||
provider,
|
||||
providerData,
|
||||
selectOptionsProviderAction,
|
||||
dispatchFetchOptions
|
||||
} = this.props;
|
||||
|
||||
if (selectOptionsProviderAction) {
|
||||
this.setState({ refetchRequired: false });
|
||||
dispatchFetchOptions({
|
||||
section: selectOptionsProviderAction,
|
||||
action: selectOptionsProviderAction,
|
||||
provider,
|
||||
providerData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
const {
|
||||
selectOptionsProviderAction,
|
||||
dispatchClearOptions
|
||||
} = this.props;
|
||||
|
||||
if (selectOptionsProviderAction) {
|
||||
dispatchClearOptions({ section: selectOptionsProviderAction });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onOpen={this.onOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EnhancedSelectInputConnector.propTypes = {
|
||||
provider: PropTypes.string.isRequired,
|
||||
providerData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
||||
dispatchClearOptions: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EnhancedSelectInputConnector);
|
||||
@@ -20,7 +20,7 @@
|
||||
.optionCheckInput {
|
||||
composes: input from '~./CheckInput.css';
|
||||
|
||||
margin-top: 0px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.isSelected {
|
||||
|
||||
@@ -32,6 +32,7 @@ class EnhancedSelectInputOption extends Component {
|
||||
const {
|
||||
className,
|
||||
id,
|
||||
depth,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isHidden,
|
||||
@@ -54,6 +55,11 @@ class EnhancedSelectInputOption extends Component {
|
||||
onPress={this.onPress}
|
||||
>
|
||||
|
||||
{
|
||||
depth !== 0 &&
|
||||
<div style={{ width: `${depth * 20}px` }} />
|
||||
}
|
||||
|
||||
{
|
||||
isMultiSelect &&
|
||||
<CheckInput
|
||||
@@ -84,6 +90,7 @@ class EnhancedSelectInputOption extends Component {
|
||||
EnhancedSelectInputOption.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isHidden: PropTypes.bool.isRequired,
|
||||
@@ -95,6 +102,7 @@ EnhancedSelectInputOption.propTypes = {
|
||||
|
||||
EnhancedSelectInputOption.defaultProps = {
|
||||
className: styles.option,
|
||||
depth: 0,
|
||||
isDisabled: false,
|
||||
isHidden: false,
|
||||
isMultiSelect: false
|
||||
|
||||
@@ -18,6 +18,7 @@ import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import TagInputConnector from './TagInputConnector';
|
||||
import TextTagInputConnector from './TextTagInputConnector';
|
||||
import TextInput from './TextInput';
|
||||
@@ -71,6 +72,9 @@ function getComponent(type) {
|
||||
case inputTypes.SELECT:
|
||||
return EnhancedSelectInput;
|
||||
|
||||
case inputTypes.DYNAMIC_SELECT:
|
||||
return EnhancedSelectInputConnector;
|
||||
|
||||
case inputTypes.SERIES_TYPE_SELECT:
|
||||
return SeriesTypeSelectInput;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ function HintedSelectInputOption(props) {
|
||||
id,
|
||||
value,
|
||||
hint,
|
||||
depth,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isMultiSelect,
|
||||
@@ -19,6 +20,7 @@ function HintedSelectInputOption(props) {
|
||||
return (
|
||||
<EnhancedSelectInputOption
|
||||
id={id}
|
||||
depth={depth}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
isHidden={isDisabled}
|
||||
@@ -48,6 +50,7 @@ HintedSelectInputOption.propTypes = {
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
hint: PropTypes.node,
|
||||
depth: PropTypes.number,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -6,7 +6,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function getType(type, value) {
|
||||
function getType({ type, selectOptionsProviderAction }) {
|
||||
switch (type) {
|
||||
case 'captcha':
|
||||
return inputTypes.CAPTCHA;
|
||||
@@ -23,6 +23,9 @@ function getType(type, value) {
|
||||
case 'filePath':
|
||||
return inputTypes.PATH;
|
||||
case 'select':
|
||||
if (selectOptionsProviderAction) {
|
||||
return inputTypes.DYNAMIC_SELECT;
|
||||
}
|
||||
return inputTypes.SELECT;
|
||||
case 'tag':
|
||||
return inputTypes.TEXT_TAG;
|
||||
@@ -85,7 +88,7 @@ function ProviderFieldFormGroup(props) {
|
||||
<FormLabel>{label}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={getType(type, value)}
|
||||
type={getType(props)}
|
||||
name={name}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
@@ -105,7 +108,8 @@ function ProviderFieldFormGroup(props) {
|
||||
|
||||
const selectOptionsShape = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.number.isRequired
|
||||
value: PropTypes.number.isRequired,
|
||||
hint: PropTypes.string
|
||||
};
|
||||
|
||||
ProviderFieldFormGroup.propTypes = {
|
||||
@@ -122,6 +126,7 @@ ProviderFieldFormGroup.propTypes = {
|
||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class FilterMenuContent extends Component {
|
||||
{
|
||||
showCustomFilters &&
|
||||
<MenuItem onPress={onCustomFiltersPress}>
|
||||
Custom Filters
|
||||
Custom Filters
|
||||
</MenuItem>
|
||||
}
|
||||
</MenuContent>
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Form from 'Components/Form/Form';
|
||||
@@ -136,7 +136,7 @@ class TableOptionsModal extends Component {
|
||||
isOpen ?
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Table Options
|
||||
Table Options
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -231,7 +231,7 @@ class TableOptionsModal extends Component {
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent> :
|
||||
|
||||
@@ -156,3 +156,35 @@
|
||||
.body {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.verticalContainer {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointExtraSmall - 20px);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointExtraSmall) {
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointSmall * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointSmall) {
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointMedium * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointMedium) {
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointLarge * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* @media only screen and (max-width: $breakpointLarge) {
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointLarge * 0.8);
|
||||
}
|
||||
} */
|
||||
|
||||
@@ -5,8 +5,25 @@ import classNames from 'classnames';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Portal from 'Components/Portal';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import styles from './Tooltip.css';
|
||||
|
||||
let maxWidth = null;
|
||||
|
||||
function getMaxWidth() {
|
||||
const windowWidth = window.innerWidth;
|
||||
|
||||
if (windowWidth >= parseInt(dimensions.breakpointLarge)) {
|
||||
maxWidth = 800;
|
||||
} else if (windowWidth >= parseInt(dimensions.breakpointMedium)) {
|
||||
maxWidth = 650;
|
||||
} else if (windowWidth >= parseInt(dimensions.breakpointSmall)) {
|
||||
maxWidth = 500;
|
||||
} else {
|
||||
maxWidth = 450;
|
||||
}
|
||||
}
|
||||
|
||||
class Tooltip extends Component {
|
||||
|
||||
//
|
||||
@@ -17,6 +34,7 @@ class Tooltip extends Component {
|
||||
|
||||
this._scheduleUpdate = null;
|
||||
this._closeTimeout = null;
|
||||
this._maxWidth = maxWidth || getMaxWidth();
|
||||
|
||||
this.state = {
|
||||
isOpen: false
|
||||
@@ -54,9 +72,11 @@ class Tooltip extends Component {
|
||||
} else if ((/^bottom/).test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom - 20;
|
||||
} else if ((/^right/).test(data.placement)) {
|
||||
data.styles.maxWidth = windowWidth - right - 50;
|
||||
data.styles.maxWidth = Math.min(this._maxWidth, windowWidth - right - 20);
|
||||
data.styles.maxHeight = top - 20;
|
||||
} else {
|
||||
data.styles.maxWidth = left - 35;
|
||||
data.styles.maxWidth = Math.min(this._maxWidth, left - 20);
|
||||
data.styles.maxHeight = top - 20;
|
||||
}
|
||||
|
||||
return data;
|
||||
@@ -144,10 +164,16 @@ class Tooltip extends Component {
|
||||
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
|
||||
this._scheduleUpdate = scheduleUpdate;
|
||||
|
||||
const popperPlacement = placement ? placement.split('-')[0] : position;
|
||||
const vertical = popperPlacement === 'top' || popperPlacement === 'bottom';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={styles.tooltipContainer}
|
||||
className={classNames(
|
||||
styles.tooltipContainer,
|
||||
vertical ? styles.verticalContainer : styles.horizontalContainer
|
||||
)}
|
||||
style={style}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
@@ -156,7 +182,7 @@ class Tooltip extends Component {
|
||||
className={this.state.isOpen ? classNames(
|
||||
styles.arrow,
|
||||
styles[kind],
|
||||
styles[placement.split('-')[0]]
|
||||
styles[popperPlacement]
|
||||
) : styles.arrowDisabled}
|
||||
ref={arrowProps.ref}
|
||||
style={arrowProps.style}
|
||||
@@ -201,7 +227,7 @@ Tooltip.defaultProps = {
|
||||
bodyClassName: styles.body,
|
||||
kind: kinds.DEFAULT,
|
||||
position: tooltipPositions.TOP,
|
||||
canFlip: true
|
||||
canFlip: false
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
|
||||
@@ -13,6 +13,7 @@ export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
|
||||
export const INDEXER_SELECT = 'indexerSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const SELECT = 'select';
|
||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||
export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
|
||||
export const TAG = 'tag';
|
||||
export const TEXT = 'text';
|
||||
@@ -34,6 +35,7 @@ export const all = [
|
||||
INDEXER_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
SELECT,
|
||||
DYNAMIC_SELECT,
|
||||
SERIES_TYPE_SELECT,
|
||||
TAG,
|
||||
TEXT,
|
||||
|
||||
@@ -193,7 +193,7 @@ function InteractiveSearch(props) {
|
||||
{
|
||||
totalReleasesCount !== items.length && !!items.length ?
|
||||
<div className={styles.filteredMessage}>
|
||||
Some results are hidden by the applied filter
|
||||
Some results are hidden by the applied filter
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@ class SeriesIndexOverview extends Component {
|
||||
seasonCount,
|
||||
episodeCount,
|
||||
episodeFileCount,
|
||||
totalEpisodeCount
|
||||
totalEpisodeCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -212,6 +213,7 @@ class SeriesIndexOverview extends Component {
|
||||
nextAiring={nextAiring}
|
||||
seasonCount={seasonCount}
|
||||
qualityProfile={qualityProfile}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
|
||||
@@ -15,7 +15,6 @@ const rows = [
|
||||
name: 'monitored',
|
||||
showProp: 'showMonitored',
|
||||
valueProp: 'monitored'
|
||||
|
||||
},
|
||||
{
|
||||
name: 'network',
|
||||
|
||||
@@ -71,7 +71,8 @@ class SeriesIndexOverviews extends Component {
|
||||
items,
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter
|
||||
jumpToCharacter,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -81,13 +82,17 @@ class SeriesIndexOverviews extends Component {
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.overviewOptions !== overviewOptions) {
|
||||
this.calculateGrid();
|
||||
this.calculateGrid(this.state.width, isSmallScreen);
|
||||
}
|
||||
|
||||
if (this._grid &&
|
||||
if (
|
||||
this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
||||
prevProps.overviewOptions !== overviewOptions
|
||||
)
|
||||
) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ function NoSeries(props) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
All series are hidden due to the applied filter.
|
||||
All series are hidden due to the applied filter.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -140,7 +140,7 @@ EditRemotePathMappingModalContent.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.shape(remotePathMappingShape).isRequired,
|
||||
downloadClientHosts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
downloadClientHosts: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
|
||||
@@ -161,7 +161,7 @@ function EditImportListModalContent(props) {
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Series Type
|
||||
Series Type
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
}
|
||||
|
||||
.footNote {
|
||||
|
||||
display: flex;
|
||||
color: $helpTextColor;
|
||||
|
||||
|
||||
@@ -468,7 +468,7 @@ class NamingModal extends Component {
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<div>
|
||||
MediaInfo Full/AudioLanguages/SubtitleLanguages support a <code>:EN+DE</code> suffix allowing you to filter the languages included in the filename. Use <code>-DE</code> to exclude specific languages.
|
||||
Appending <code>+</code> (eg <code>:EN+</code>) will output <code>[EN]</code>/<code>[EN+--]</code>/<code>[--]</code> depending on excluded languages. For example <code>{'{'}MediaInfo Full:EN+DE{'}'}</code>.
|
||||
Appending <code>+</code> (eg <code>:EN+</code>) will output <code>[EN]</code>/<code>[EN+--]</code>/<code>[--]</code> depending on excluded languages. For example <code>{'{'}MediaInfo Full:EN+DE{'}'}</code>.
|
||||
</div>
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
|
||||
@@ -176,7 +176,7 @@ class EditQualityProfileModalContent extends Component {
|
||||
upgradeAllowed.value &&
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Upgrade Until
|
||||
Upgrade Until
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
.track {
|
||||
top: 9px;
|
||||
margin: 0 5px;
|
||||
height: 3px;
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
.thumb {
|
||||
top: 1px;
|
||||
z-index: 0 !important;
|
||||
width: 18px;
|
||||
|
||||
@@ -68,6 +68,27 @@ class QualityDefinition extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
trackRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.track}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
thumbRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.thumb}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -162,16 +183,16 @@ class QualityDefinition extends Component {
|
||||
|
||||
<div className={styles.sizeLimit}>
|
||||
<ReactSlider
|
||||
className={styles.slider}
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
minDistance={10}
|
||||
value={[sliderMinSize, sliderMaxSize]}
|
||||
withBars={true}
|
||||
withTracks={true}
|
||||
snapDragDisabled={true}
|
||||
className={styles.slider}
|
||||
barClassName={styles.bar}
|
||||
handleClassName={styles.handle}
|
||||
renderThumb={this.thumbRenderer}
|
||||
renderTrack={this.trackRenderer}
|
||||
onChange={this.onSliderChange}
|
||||
onAfterChange={this.onAfterSliderChange}
|
||||
/>
|
||||
|
||||
@@ -68,6 +68,17 @@ function Settings() {
|
||||
Download clients, download handling and remote path mappings
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/importlists"
|
||||
>
|
||||
Import Lists
|
||||
</Link>
|
||||
|
||||
<div className={styles.summary}>
|
||||
Import from another Sonarr instance or Trakt lists and manage list exclusions
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/connect"
|
||||
|
||||
@@ -167,7 +167,7 @@ function TagDetailsModalContent(props) {
|
||||
isDisabled={isTagUsed}
|
||||
onPress={onDeleteTagPress}
|
||||
>
|
||||
Delete
|
||||
Delete
|
||||
</Button>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import requestAction from 'Utilities/requestAction';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
@@ -10,6 +11,9 @@ import { set } from './baseActions';
|
||||
|
||||
export const section = 'providerOptions';
|
||||
|
||||
const lastActions = {};
|
||||
let lastActionId = 0;
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
@@ -23,8 +27,8 @@ export const defaultState = {
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_OPTIONS = 'devices/fetchOptions';
|
||||
export const CLEAR_OPTIONS = 'devices/clearOptions';
|
||||
export const FETCH_OPTIONS = 'providers/fetchOptions';
|
||||
export const CLEAR_OPTIONS = 'providers/clearOptions';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
@@ -38,30 +42,55 @@ export const clearOptions = createAction(CLEAR_OPTIONS);
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[FETCH_OPTIONS]: function(getState, payload, dispatch) {
|
||||
const subsection = `${section}.${payload.section}`;
|
||||
|
||||
if (lastActions[payload.section] && _.isEqual(payload, lastActions[payload.section].payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionId = ++lastActionId;
|
||||
|
||||
lastActions[payload.section] = {
|
||||
actionId,
|
||||
payload
|
||||
};
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
section: subsection,
|
||||
isFetching: true
|
||||
}));
|
||||
|
||||
const promise = requestAction(payload);
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null,
|
||||
items: data.options || []
|
||||
}));
|
||||
if (lastActions[payload.section]) {
|
||||
if (lastActions[payload.section].actionId === actionId) {
|
||||
lastActions[payload.section] = null;
|
||||
}
|
||||
|
||||
dispatch(set({
|
||||
section: subsection,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null,
|
||||
items: data.options || []
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr
|
||||
}));
|
||||
if (lastActions[payload.section]) {
|
||||
if (lastActions[payload.section].actionId === actionId) {
|
||||
lastActions[payload.section] = null;
|
||||
}
|
||||
|
||||
dispatch(set({
|
||||
section: subsection,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -71,8 +100,12 @@ export const actionHandlers = handleThunks({
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_OPTIONS]: function(state) {
|
||||
return updateSectionState(state, section, defaultState);
|
||||
[CLEAR_OPTIONS]: function(state, { payload }) {
|
||||
const subsection = `${section}.${payload.section}`;
|
||||
|
||||
lastActions[payload.section] = null;
|
||||
|
||||
return updateSectionState(state, subsection, defaultState);
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
}, {}, section);
|
||||
|
||||
@@ -200,7 +200,7 @@ class RestoreBackupModalContent extends Component {
|
||||
</div>
|
||||
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -209,7 +209,7 @@ class RestoreBackupModalContent extends Component {
|
||||
isSpinning={isRestoring}
|
||||
onPress={this.onRestorePress}
|
||||
>
|
||||
Restore
|
||||
Restore
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -82,7 +82,7 @@ function LogsTable(props) {
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
No events found
|
||||
No events found
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const monitorOptions = [
|
||||
{ key: 'future', value: 'Future Episodes' },
|
||||
{ key: 'missing', value: 'Missing Episodes' },
|
||||
{ key: 'existing', value: 'Existing Episodes' },
|
||||
{ key: 'pilot', value: 'Pilot Episode' },
|
||||
{ key: 'firstSeason', value: 'Only First Season' },
|
||||
{ key: 'latestSeason', value: 'Only Latest Season' },
|
||||
{ key: 'none', value: 'None' }
|
||||
|
||||
132
package.json
132
package.json
@@ -15,69 +15,69 @@
|
||||
"license": "GPL-3.0",
|
||||
"readmeFilename": "readme.md",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.5.4",
|
||||
"@babel/plugin-proposal-class-properties": "7.5.0",
|
||||
"@babel/plugin-proposal-decorators": "7.4.4",
|
||||
"@babel/plugin-proposal-export-default-from": "7.5.2",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.5.2",
|
||||
"@babel/plugin-proposal-function-sent": "7.5.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.2.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.2.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.2.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.2.0",
|
||||
"@babel/preset-env": "7.5.4",
|
||||
"@babel/preset-react": "7.0.0",
|
||||
"@fortawesome/fontawesome-free": "5.9.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.19",
|
||||
"@fortawesome/free-regular-svg-icons": "5.9.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.9.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.4",
|
||||
"@sentry/browser": "5.5.0",
|
||||
"@sentry/integrations": "5.5.0",
|
||||
"@babel/core": "7.11.6",
|
||||
"@babel/plugin-proposal-class-properties": "7.10.4",
|
||||
"@babel/plugin-proposal-decorators": "7.10.5",
|
||||
"@babel/plugin-proposal-export-default-from": "7.10.4",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.10.4",
|
||||
"@babel/plugin-proposal-function-sent": "7.10.4",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.10.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.10.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.11.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.10.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.11.5",
|
||||
"@babel/preset-react": "7.10.4",
|
||||
"@fortawesome/fontawesome-free": "5.15.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.31",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.11",
|
||||
"@sentry/browser": "5.24.2",
|
||||
"@sentry/integrations": "5.24.2",
|
||||
"ansi-colors": "4.1.1",
|
||||
"autoprefixer": "9.6.1",
|
||||
"babel-eslint": "10.0.2",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"classnames": "2.2.6",
|
||||
"clipboard": "2.0.4",
|
||||
"connected-react-router": "6.5.2",
|
||||
"core-js": "3",
|
||||
"clipboard": "2.0.6",
|
||||
"connected-react-router": "6.8.0",
|
||||
"core-js": "3.6.5",
|
||||
"create-react-class": "15.6.3",
|
||||
"css-loader": "3.0.0",
|
||||
"del": "5.0.0",
|
||||
"del": "6.0.0",
|
||||
"element-class": "0.2.2",
|
||||
"esformatter": "0.10.0",
|
||||
"eslint": "6.0.1",
|
||||
"esformatter": "0.11.3",
|
||||
"eslint": "7.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-react": "7.14.2",
|
||||
"esprint": "0.4.0",
|
||||
"file-loader": "4.0.0",
|
||||
"filesize": "4.1.2",
|
||||
"fuse.js": "3.4.5",
|
||||
"eslint-plugin-react": "7.21.3",
|
||||
"esprint": "0.7.0",
|
||||
"file-loader": "6.1.0",
|
||||
"filesize": "6.1.0",
|
||||
"fuse.js": "6.4.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cached": "1.1.1",
|
||||
"gulp-concat": "2.6.1",
|
||||
"gulp-declare": "0.3.0",
|
||||
"gulp-livereload": "4.0.1",
|
||||
"gulp-livereload": "4.0.2",
|
||||
"gulp-postcss": "8.0.0",
|
||||
"gulp-print": "5.0.2",
|
||||
"gulp-sourcemaps": "2.6.5",
|
||||
"gulp-stripbom": "1.0.4",
|
||||
"gulp-stripbom": "1.0.5",
|
||||
"gulp-watch": "5.0.1",
|
||||
"gulp-wrap": "0.15.0",
|
||||
"history": "4.9.0",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.4.1",
|
||||
"loader-utils": "^1.1.0",
|
||||
"lodash": "4.17.14",
|
||||
"jquery": "3.5.1",
|
||||
"loader-utils": "^2.0.0",
|
||||
"lodash": "4.17.20",
|
||||
"mini-css-extract-plugin": "0.8.0",
|
||||
"mobile-detect": "1.4.3",
|
||||
"moment": "2.24.0",
|
||||
"mousetrap": "1.6.3",
|
||||
"mobile-detect": "1.4.4",
|
||||
"moment": "2.29.0",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
@@ -86,31 +86,31 @@
|
||||
"postcss-simple-vars": "5.0.2",
|
||||
"postcss-url": "8.0.0",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.7.0",
|
||||
"react": "16.8.6",
|
||||
"qs": "6.9.4",
|
||||
"react": "16.13.1",
|
||||
"react-addons-shallow-compare": "15.6.2",
|
||||
"react-async-script": "1.1.1",
|
||||
"react-autosuggest": "9.4.3",
|
||||
"react-async-script": "1.2.0",
|
||||
"react-autosuggest": "10.0.2",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
"react-dnd": "9.3.2",
|
||||
"react-dnd-html5-backend": "9.3.2",
|
||||
"react-dnd": "11.1.3",
|
||||
"react-dnd-html5-backend": "11.1.3",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "16.8.6",
|
||||
"react-focus-lock": "2.2.1",
|
||||
"react-google-recaptcha": "1.1.0",
|
||||
"react-lazyload": "2.6.2",
|
||||
"react-dom": "16.13.1",
|
||||
"react-focus-lock": "2.4.1",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.0.0",
|
||||
"react-measure": "1.4.7",
|
||||
"react-popper": "1.3.3",
|
||||
"react-redux": "7.1.0",
|
||||
"react-router": "5.0.1",
|
||||
"react-router-dom": "5.0.1",
|
||||
"react-slider": "0.11.2",
|
||||
"react-tabs": "3.0.0",
|
||||
"react-text-truncate": "0.14.1",
|
||||
"react-redux": "7.2.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-slider": "1.0.11",
|
||||
"react-tabs": "3.1.1",
|
||||
"react-text-truncate": "0.16.0",
|
||||
"react-virtualized": "9.21.1",
|
||||
"redux": "4.0.4",
|
||||
"redux": "4.0.5",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.4.1",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
"redux-localstorage": "0.4.1",
|
||||
"redux-thunk": "2.3.0",
|
||||
"require-nocache": "1.0.0",
|
||||
@@ -119,12 +119,12 @@
|
||||
"signalr": "2.4.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "0.23.1",
|
||||
"stylelint": "10.1.0",
|
||||
"stylelint-order": "3.0.1",
|
||||
"url-loader": "2.0.1",
|
||||
"webpack": "4.35.3",
|
||||
"webpack-stream": "5.2.1",
|
||||
"worker-loader": "2.0.0"
|
||||
"stylelint": "13.7.2",
|
||||
"stylelint-order": "4.1.0",
|
||||
"url-loader": "4.1.0",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-stream": "6.1.0",
|
||||
"worker-loader": "3.0.3"
|
||||
},
|
||||
"main": "index.js",
|
||||
"browserslist": [
|
||||
|
||||
@@ -402,6 +402,58 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
VerifyCopyFolder(source.FullName, destination.FullName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CopyFolder_should_detect_caseinsensitive_parents()
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
WithRealDiskProvider();
|
||||
|
||||
var original = GetFilledTempFolder();
|
||||
var root = new DirectoryInfo(GetTempFilePath());
|
||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
||||
var destination = new DirectoryInfo(root.FullName + "a/series");
|
||||
|
||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
||||
|
||||
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CopyFolder_should_detect_caseinsensitive_folder()
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
WithRealDiskProvider();
|
||||
|
||||
var original = GetFilledTempFolder();
|
||||
var root = new DirectoryInfo(GetTempFilePath());
|
||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
||||
var destination = new DirectoryInfo(root.FullName + "A/Series");
|
||||
|
||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
||||
|
||||
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CopyFolder_should_not_copy_casesensitive_folder()
|
||||
{
|
||||
MonoOnly();
|
||||
|
||||
WithRealDiskProvider();
|
||||
|
||||
var original = GetFilledTempFolder();
|
||||
var root = new DirectoryInfo(GetTempFilePath());
|
||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
||||
var destination = new DirectoryInfo(root.FullName + "A/Series");
|
||||
|
||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
||||
|
||||
// Note: Although technically possible top copy to different case, we're not allowing it
|
||||
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CopyFolder_should_ignore_nfs_temp_file()
|
||||
{
|
||||
@@ -454,6 +506,8 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
[Test]
|
||||
public void MoveFolder_should_detect_caseinsensitive_parents()
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
WithRealDiskProvider();
|
||||
|
||||
var original = GetFilledTempFolder();
|
||||
@@ -469,6 +523,8 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
[Test]
|
||||
public void MoveFolder_should_rename_caseinsensitive_folder()
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
WithRealDiskProvider();
|
||||
|
||||
var original = GetFilledTempFolder();
|
||||
@@ -483,6 +539,26 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
source.FullName.GetActualCasing().Should().Be(destination.FullName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MoveFolder_should_rename_casesensitive_folder()
|
||||
{
|
||||
MonoOnly();
|
||||
|
||||
WithRealDiskProvider();
|
||||
|
||||
var original = GetFilledTempFolder();
|
||||
var root = new DirectoryInfo(GetTempFilePath());
|
||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
||||
var destination = new DirectoryInfo(root.FullName + "A/Series");
|
||||
|
||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
||||
|
||||
Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move);
|
||||
|
||||
Directory.Exists(source.FullName).Should().Be(false);
|
||||
Directory.Exists(destination.FullName).Should().Be(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_if_destination_is_readonly()
|
||||
{
|
||||
@@ -585,6 +661,23 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
VerifyCopyFolder(original.FullName, destination.FullName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MirrorFolder_should_handle_trailing_slash()
|
||||
{
|
||||
WithRealDiskProvider();
|
||||
|
||||
var original = GetFilledTempFolder();
|
||||
var source = new DirectoryInfo(GetTempFilePath());
|
||||
var destination = new DirectoryInfo(GetTempFilePath());
|
||||
|
||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
||||
|
||||
var count = Subject.MirrorFolder(source.FullName + Path.DirectorySeparatorChar, destination.FullName);
|
||||
|
||||
count.Should().Equals(3);
|
||||
VerifyCopyFolder(original.FullName, destination.FullName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TransferFolder_should_use_movefolder_if_on_same_mount()
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ using NzbDrone.Test.Common.Categories;
|
||||
|
||||
namespace NzbDrone.Common.Test.Http
|
||||
{
|
||||
[Ignore("httpbin is bugged")]
|
||||
[IntegrationTest]
|
||||
[TestFixture(typeof(ManagedHttpDispatcher))]
|
||||
public class HttpClientFixture<TDispatcher> : TestBase<HttpClient> where TDispatcher : IHttpDispatcher
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace NzbDrone.Common.Test.Http
|
||||
public class HttpUriFixture : TestBase
|
||||
{
|
||||
[TestCase("abc://my_host.com:8080/root/api/")]
|
||||
[TestCase("abc://my_host.com:8080//root/api/")]
|
||||
[TestCase("abc://my_host.com:8080/root//api/")]
|
||||
public void should_parse(string uri)
|
||||
{
|
||||
var newUri = new HttpUri(uri);
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Common.Disk
|
||||
@@ -32,13 +29,16 @@ namespace NzbDrone.Common.Disk
|
||||
private string ResolveRealParentPath(string path)
|
||||
{
|
||||
var parentPath = path.GetParentPath();
|
||||
if (!_diskProvider.FolderExists(path))
|
||||
if (!_diskProvider.FolderExists(parentPath))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
parentPath = parentPath.GetActualCasing();
|
||||
return parentPath + Path.DirectorySeparatorChar + Path.GetFileName(path);
|
||||
var realParentPath = parentPath.GetActualCasing();
|
||||
|
||||
var partialChildPath = path.Substring(parentPath.Length);
|
||||
|
||||
return realParentPath + partialChildPath;
|
||||
}
|
||||
|
||||
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
|
||||
@@ -245,17 +245,15 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
_logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath);
|
||||
|
||||
var originalSize = _diskProvider.GetFileSize(sourcePath);
|
||||
|
||||
if (sourcePath == targetPath)
|
||||
{
|
||||
throw new IOException(string.Format("Source and destination can't be the same {0}", sourcePath));
|
||||
}
|
||||
|
||||
var originalSize = _diskProvider.GetFileSize(sourcePath);
|
||||
|
||||
if (sourcePath.PathEquals(targetPath, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
// Shortcut for dealing with inplace rename
|
||||
|
||||
if (mode.HasFlag(TransferMode.HardLink) || mode.HasFlag(TransferMode.Copy))
|
||||
{
|
||||
throw new IOException(string.Format("Source and destination can't be the same {0}", sourcePath));
|
||||
@@ -268,7 +266,7 @@ namespace NzbDrone.Common.Disk
|
||||
_diskProvider.MoveFile(sourcePath, tempPath, true);
|
||||
try
|
||||
{
|
||||
ClearTargetPath(targetPath, overwrite);
|
||||
ClearTargetPath(sourcePath, targetPath, overwrite);
|
||||
|
||||
_diskProvider.MoveFile(tempPath, targetPath);
|
||||
|
||||
@@ -298,77 +296,7 @@ namespace NzbDrone.Common.Disk
|
||||
throw new IOException(string.Format("Destination cannot be a child of the source [{0}] => [{1}]", sourcePath, targetPath));
|
||||
}
|
||||
|
||||
var targetFileExists = _diskProvider.FileExists(targetPath);
|
||||
var targetFilePotentiallySame = targetFileExists && Path.GetFileName(sourcePath).EqualsIgnoreCase(Path.GetFileName(targetPath)) && _diskProvider.GetFileSize(targetPath) == originalSize;
|
||||
|
||||
// If the target file exists and has the same name ans size then it _could_ be that they're actually pointing to the same actual file via softlinks.
|
||||
// This is reasonably easy to determine in linux, but on windows it's more tricky.
|
||||
// Depending on 'overwrite' and Move/Copy we have to take different actions.
|
||||
|
||||
if (targetFileExists)
|
||||
{
|
||||
if (targetFilePotentiallySame)
|
||||
{
|
||||
var targetBackupPath = targetPath + ".backup~";
|
||||
|
||||
if (mode.HasFlag(TransferMode.HardLink) || mode.HasFlag(TransferMode.Copy))
|
||||
{
|
||||
// Copy doesn't allow renames, only overwrite
|
||||
if (!overwrite)
|
||||
{
|
||||
throw new DestinationAlreadyExistsException($"Destination {targetPath} already exists.");
|
||||
}
|
||||
|
||||
_diskProvider.MoveFile(targetPath, targetBackupPath, true);
|
||||
if (!_diskProvider.FileExists(sourcePath))
|
||||
{
|
||||
// They were the same file, Copy/Hardlink can't handle that, so revert and throw
|
||||
// Note that on windows this can actually cause the casing to change
|
||||
_diskProvider.MoveFile(targetBackupPath, targetPath);
|
||||
|
||||
throw new IOException(string.Format("Source and destination can't be the same {0}", sourcePath));
|
||||
}
|
||||
}
|
||||
else if (mode.HasFlag(TransferMode.Move))
|
||||
{
|
||||
// Move allows a rename of same file, or overwrite different file
|
||||
_diskProvider.MoveFile(targetPath, targetBackupPath, true);
|
||||
if (!_diskProvider.FileExists(sourcePath))
|
||||
{
|
||||
// They were the same file, treat this as if it's a rename in place
|
||||
_diskProvider.MoveFile(targetBackupPath, targetPath);
|
||||
|
||||
return TransferMode.Move;
|
||||
}
|
||||
else
|
||||
{
|
||||
// They were different files, only allow the move if overwrite is enabled, otherwise revert and throw
|
||||
if (overwrite)
|
||||
{
|
||||
_diskProvider.DeleteFile(targetBackupPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_diskProvider.MoveFile(targetBackupPath, targetPath);
|
||||
throw new DestinationAlreadyExistsException($"Destination {targetPath} already exists.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return TransferMode.None;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!overwrite)
|
||||
{
|
||||
throw new DestinationAlreadyExistsException($"Destination {targetPath} already exists.");
|
||||
}
|
||||
|
||||
_diskProvider.DeleteFile(targetPath);
|
||||
}
|
||||
}
|
||||
ClearTargetPath(sourcePath, targetPath, overwrite);
|
||||
|
||||
if (mode.HasFlag(TransferMode.HardLink))
|
||||
{
|
||||
@@ -394,7 +322,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
var isCifs = targetDriveFormat == "cifs";
|
||||
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
||||
|
||||
|
||||
if (mode.HasFlag(TransferMode.Copy))
|
||||
{
|
||||
if (isBtrfs)
|
||||
@@ -435,7 +363,7 @@ namespace NzbDrone.Common.Disk
|
||||
_diskProvider.DeleteFile(sourcePath);
|
||||
return TransferMode.Move;
|
||||
}
|
||||
|
||||
|
||||
TryMoveFileVerified(sourcePath, targetPath, originalSize);
|
||||
return TransferMode.Move;
|
||||
}
|
||||
@@ -443,7 +371,7 @@ namespace NzbDrone.Common.Disk
|
||||
return TransferMode.None;
|
||||
}
|
||||
|
||||
private void ClearTargetPath(string targetPath, bool overwrite)
|
||||
private void ClearTargetPath(string sourcePath, string targetPath, bool overwrite)
|
||||
{
|
||||
if (_diskProvider.FileExists(targetPath))
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
public class HttpUri : IEquatable<HttpUri>
|
||||
{
|
||||
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+)(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/)[^/?#\r\n]+)+/?|/)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+)(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private readonly string _uri;
|
||||
public string FullUri => _uri;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
[Test]
|
||||
public void should_parse_recent_feed_from_FileList()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/FileList/recentfeed.json");
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/FileList/RecentFeed.json");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
|
||||
@@ -288,7 +288,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_airDate_if_series_isDaily()
|
||||
public void should_use_airDate_if_series_isDaily_and_not_a_special()
|
||||
{
|
||||
_namingConfig.DailyEpisodeFormat = "{Series Title} - {air-date} - {Episode Title}";
|
||||
|
||||
@@ -297,11 +297,34 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
|
||||
_episode1.AirDate = "2012-12-13";
|
||||
_episode1.Title = "Kristen Stewart";
|
||||
_episode1.SeasonNumber = 1;
|
||||
_episode1.EpisodeNumber = 5;
|
||||
|
||||
_episodeFile.SeasonNumber = 1;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
|
||||
.Should().Be("The Daily Show with Jon Stewart - 2012-12-13 - Kristen Stewart");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_standard_if_series_isDaily_special()
|
||||
{
|
||||
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}";
|
||||
|
||||
_series.Title = "The Daily Show with Jon Stewart";
|
||||
_series.SeriesType = SeriesTypes.Daily;
|
||||
|
||||
_episode1.AirDate = "2012-12-13";
|
||||
_episode1.Title = "Kristen Stewart";
|
||||
_episode1.SeasonNumber = 0;
|
||||
_episode1.EpisodeNumber = 5;
|
||||
|
||||
_episodeFile.SeasonNumber = 0;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
|
||||
.Should().Be("The Daily Show with Jon Stewart - S00E05 - Kristen Stewart");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_airdate_to_unknown_if_not_available()
|
||||
{
|
||||
@@ -312,6 +335,10 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
|
||||
_episode1.AirDate = null;
|
||||
_episode1.Title = "Kristen Stewart";
|
||||
_episode1.SeasonNumber = 1;
|
||||
_episode1.EpisodeNumber = 5;
|
||||
|
||||
_episodeFile.SeasonNumber = 1;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
|
||||
.Should().Be("The Daily Show with Jon Stewart - Unknown - Kristen Stewart");
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Annotations
|
||||
public FieldType Type { get; set; }
|
||||
public bool Advanced { get; set; }
|
||||
public Type SelectOptions { get; set; }
|
||||
public string SelectOptionsProviderAction { get; set; }
|
||||
public string Section { get; set; }
|
||||
public HiddenType Hidden { get; set; }
|
||||
public PrivacyLevel Privacy { get; set; }
|
||||
@@ -38,6 +39,15 @@ namespace NzbDrone.Core.Annotations
|
||||
public string Hint { get; set; }
|
||||
}
|
||||
|
||||
public class FieldSelectOption
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Order { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public int? ParentValue { get; set; }
|
||||
}
|
||||
|
||||
public enum FieldType
|
||||
{
|
||||
Textbox,
|
||||
|
||||
@@ -70,14 +70,23 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
return SearchDaily(series, episode, userInvokedSearch, interactiveSearch);
|
||||
}
|
||||
|
||||
if (series.SeriesType == SeriesTypes.Anime)
|
||||
{
|
||||
if (episode.SeasonNumber == 0 &&
|
||||
episode.SceneAbsoluteEpisodeNumber == null &&
|
||||
episode.AbsoluteEpisodeNumber == null)
|
||||
{
|
||||
// Search for special episodes in season 0 that don't have absolute episode numbers
|
||||
return SearchSpecial(series, new List<Episode> { episode }, userInvokedSearch, interactiveSearch);
|
||||
}
|
||||
|
||||
return SearchAnime(series, episode, userInvokedSearch, interactiveSearch);
|
||||
}
|
||||
|
||||
if (episode.SeasonNumber == 0)
|
||||
{
|
||||
// search for special episodes in season 0
|
||||
// Search for special episodes in season 0
|
||||
return SearchSpecial(series, new List<Episode> { episode }, userInvokedSearch, interactiveSearch);
|
||||
}
|
||||
|
||||
@@ -226,13 +235,23 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
private List<DownloadDecision> SearchSpecial(Series series, List<Episode> episodes, bool userInvokedSearch, bool interactiveSearch)
|
||||
{
|
||||
var downloadDecisions = new List<DownloadDecision>();
|
||||
|
||||
var searchSpec = Get<SpecialEpisodeSearchCriteria>(series, episodes, userInvokedSearch, interactiveSearch);
|
||||
// build list of queries for each episode in the form: "<series> <episode-title>"
|
||||
searchSpec.EpisodeQueryTitles = episodes.Where(e => !string.IsNullOrWhiteSpace(e.Title))
|
||||
.SelectMany(e => searchSpec.QueryTitles.Select(title => title + " " + SearchCriteriaBase.GetQueryTitle(e.Title)))
|
||||
.ToArray();
|
||||
|
||||
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
|
||||
downloadDecisions.AddRange(Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec));
|
||||
|
||||
// Search for each episode by season/episode number as well
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
downloadDecisions.AddRange(SearchSingle(series, episode, userInvokedSearch, interactiveSearch));
|
||||
}
|
||||
|
||||
return downloadDecisions;
|
||||
}
|
||||
|
||||
private List<DownloadDecision> SearchAnimeSeason(Series series, List<Episode> episodes, bool userInvokedSearch, bool interactiveSearch)
|
||||
|
||||
@@ -332,7 +332,14 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
var parser = GetParser();
|
||||
var generator = GetRequestGenerator();
|
||||
var releases = FetchPage(generator.GetRecentRequests().GetAllTiers().First().First(), parser);
|
||||
var firstRequest = generator.GetRecentRequests().GetAllTiers().FirstOrDefault()?.FirstOrDefault();
|
||||
|
||||
if (firstRequest == null)
|
||||
{
|
||||
return new ValidationFailure(string.Empty, "No rss feed query available. This may be an issue with the indexer or your indexer category settings.");
|
||||
}
|
||||
|
||||
var releases = FetchPage(firstRequest, parser);
|
||||
|
||||
if (releases.Empty())
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -133,5 +134,31 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||
}
|
||||
}
|
||||
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "newznabCategories")
|
||||
{
|
||||
List<NewznabCategory> categories = null;
|
||||
try
|
||||
{
|
||||
if (Settings.BaseUrl.IsNotNullOrWhiteSpace() && Settings.ApiPath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
categories = _capabilitiesProvider.GetCapabilities(Settings).Categories;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Use default categories
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
options = NewznabCategoryFieldOptionsConverter.GetFieldSelectOptions(categories)
|
||||
};
|
||||
}
|
||||
|
||||
return base.RequestAction(action, query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
@@ -49,6 +50,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
|
||||
var request = new HttpRequest(url, HttpAccept.Rss);
|
||||
request.AllowAutoRedirect = true;
|
||||
|
||||
HttpResponse response;
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web.UI.WebControls;
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public static class NewznabCategoryFieldOptionsConverter
|
||||
{
|
||||
public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory> categories)
|
||||
{
|
||||
// Ignore categories not relevant for Sonarr
|
||||
var ignoreCategories = new[] { 0, 1000, 2000, 3000, 4000, 6000, 7000 };
|
||||
|
||||
var result = new List<FieldSelectOption>();
|
||||
|
||||
if (categories == null)
|
||||
{
|
||||
// Fetching categories failed, use default Newznab categories
|
||||
categories = new List<NewznabCategory>();
|
||||
categories.Add(new NewznabCategory
|
||||
{
|
||||
Id = 5000,
|
||||
Name = "TV",
|
||||
Subcategories = new List<NewznabCategory>
|
||||
{
|
||||
new NewznabCategory { Id = 5070, Name = "Anime" },
|
||||
new NewznabCategory { Id = 5080, Name = "Documentary" },
|
||||
new NewznabCategory { Id = 5020, Name = "Foreign" },
|
||||
new NewznabCategory { Id = 5040, Name = "HD" },
|
||||
new NewznabCategory { Id = 5045, Name = "UHD" },
|
||||
new NewznabCategory { Id = 5050, Name = "Other" },
|
||||
new NewznabCategory { Id = 5030, Name = "SD" },
|
||||
new NewznabCategory { Id = 5060, Name = "Sport" },
|
||||
new NewznabCategory { Id = 5010, Name = "WEB-DL" }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (ignoreCategories.Contains(category.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new FieldSelectOption
|
||||
{
|
||||
Value = category.Id,
|
||||
Name = category.Name,
|
||||
Hint = $"({category.Id})"
|
||||
});
|
||||
|
||||
if (category.Subcategories != null)
|
||||
{
|
||||
foreach (var subcat in category.Subcategories)
|
||||
{
|
||||
result.Add(new FieldSelectOption
|
||||
{
|
||||
Value = subcat.Id,
|
||||
Name = subcat.Name,
|
||||
Hint = $"({subcat.Id})",
|
||||
ParentValue = category.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.Sort((l, r) => l.Value.CompareTo(r.Value));
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,10 +73,10 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows")]
|
||||
[FieldDefinition(3, Label = "Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows")]
|
||||
public IEnumerable<int> Categories { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Anime Categories", HelpText = "Comma Separated list, leave blank to disable anime")]
|
||||
[FieldDefinition(4, Label = "Anime Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Comma Separated list, leave blank to disable anime")]
|
||||
public IEnumerable<int> AnimeCategories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
||||
|
||||
@@ -55,17 +55,17 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
private IndexerDefinition GetDefinition(string name, TorznabSettings settings)
|
||||
{
|
||||
return new IndexerDefinition
|
||||
{
|
||||
EnableRss = false,
|
||||
EnableAutomaticSearch = false,
|
||||
EnableInteractiveSearch = false,
|
||||
Name = name,
|
||||
Implementation = GetType().Name,
|
||||
Settings = settings,
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
SupportsRss = SupportsRss,
|
||||
SupportsSearch = SupportsSearch
|
||||
};
|
||||
{
|
||||
EnableRss = false,
|
||||
EnableAutomaticSearch = false,
|
||||
EnableInteractiveSearch = false,
|
||||
Name = name,
|
||||
Implementation = GetType().Name,
|
||||
Settings = settings,
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
SupportsRss = SupportsRss,
|
||||
SupportsSearch = SupportsSearch
|
||||
};
|
||||
}
|
||||
|
||||
private TorznabSettings GetSettings(string url, string apiPath = null, int[] categories = null, int[] animeCategories = null)
|
||||
@@ -124,5 +124,28 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||
}
|
||||
}
|
||||
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "newznabCategories")
|
||||
{
|
||||
List<NewznabCategory> categories = null;
|
||||
try
|
||||
{
|
||||
categories = _capabilitiesProvider.GetCapabilities(Settings).Categories;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Use default categories
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
options = NewznabCategoryFieldOptionsConverter.GetFieldSelectOptions(categories)
|
||||
};
|
||||
}
|
||||
|
||||
return base.RequestAction(action, query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
|
||||
var payload = new WebhookGrabPayload
|
||||
{
|
||||
EventType = "Grab",
|
||||
EventType = WebhookEventType.Grab,
|
||||
Series = new WebhookSeries(message.Series),
|
||||
Episodes = remoteEpisode.Episodes.ConvertAll(x => new WebhookEpisode(x)),
|
||||
Release = new WebhookRelease(quality, remoteEpisode),
|
||||
@@ -43,7 +43,7 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
|
||||
var payload = new WebhookImportPayload
|
||||
{
|
||||
EventType = "Download",
|
||||
EventType = WebhookEventType.Download,
|
||||
Series = new WebhookSeries(message.Series),
|
||||
Episodes = episodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x)),
|
||||
EpisodeFile = new WebhookEpisodeFile(episodeFile),
|
||||
@@ -67,15 +67,29 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
|
||||
public override void OnRename(Series series)
|
||||
{
|
||||
var payload = new WebhookPayload
|
||||
var payload = new WebhookRenamePayload
|
||||
{
|
||||
EventType = "Rename",
|
||||
EventType = WebhookEventType.Rename,
|
||||
Series = new WebhookSeries(series)
|
||||
};
|
||||
|
||||
_proxy.SendWebhook(payload, Settings);
|
||||
}
|
||||
|
||||
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
|
||||
{
|
||||
var payload = new WebhookHealthPayload
|
||||
{
|
||||
EventType = WebhookEventType.Health,
|
||||
Level = healthCheck.Type,
|
||||
Message = healthCheck.Message,
|
||||
Type = healthCheck.Source.Name,
|
||||
WikiUrl = healthCheck.WikiUrl?.ToString()
|
||||
};
|
||||
|
||||
_proxy.SendWebhook(payload, Settings);
|
||||
}
|
||||
|
||||
public override string Name => "Webhook";
|
||||
|
||||
public override ValidationResult Test()
|
||||
@@ -93,7 +107,7 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
{
|
||||
var payload = new WebhookGrabPayload
|
||||
{
|
||||
EventType = "Test",
|
||||
EventType = WebhookEventType.Test,
|
||||
Series = new WebhookSeries()
|
||||
{
|
||||
Id = 1,
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
QualityVersion = episodeFile.Quality.Revision.Version;
|
||||
ReleaseGroup = episodeFile.ReleaseGroup;
|
||||
SceneName = episodeFile.SceneName;
|
||||
Size = episodeFile.Size;
|
||||
}
|
||||
|
||||
public int Id { get; set; }
|
||||
@@ -24,5 +25,6 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
public int QualityVersion { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public string SceneName { get; set; }
|
||||
public long Size { get; set; }
|
||||
}
|
||||
}
|
||||
17
src/NzbDrone.Core/Notifications/Webhook/WebhookEventType.cs
Normal file
17
src/NzbDrone.Core/Notifications/Webhook/WebhookEventType.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Webhook
|
||||
{
|
||||
// TODO: In v4 this will likely be changed to the default camel case.
|
||||
[JsonConverter(typeof(StringEnumConverter), converterParameters: typeof(DefaultNamingStrategy))]
|
||||
public enum WebhookEventType
|
||||
{
|
||||
Test,
|
||||
Grab,
|
||||
Download,
|
||||
Rename,
|
||||
Health
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
{
|
||||
public class WebhookGrabPayload : WebhookPayload
|
||||
{
|
||||
public WebhookSeries Series { get; set; }
|
||||
public List<WebhookEpisode> Episodes { get; set; }
|
||||
public WebhookRelease Release { get; set; }
|
||||
public string DownloadClient { get; set; }
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using NzbDrone.Core.HealthCheck;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Webhook
|
||||
{
|
||||
public class WebhookHealthPayload : WebhookPayload
|
||||
{
|
||||
public HealthCheckResult Level { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string WikiUrl { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
{
|
||||
public class WebhookImportPayload : WebhookPayload
|
||||
{
|
||||
public WebhookSeries Series { get; set; }
|
||||
public List<WebhookEpisode> Episodes { get; set; }
|
||||
public WebhookEpisodeFile EpisodeFile { get; set; }
|
||||
public bool IsUpgrade { get; set; }
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
{
|
||||
public class WebhookPayload
|
||||
{
|
||||
public string EventType { get; set; }
|
||||
public WebhookSeries Series { get; set; }
|
||||
public WebhookEventType EventType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace NzbDrone.Core.Notifications.Webhook
|
||||
{
|
||||
public class WebhookRenamePayload : WebhookPayload
|
||||
{
|
||||
public WebhookSeries Series { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ namespace NzbDrone.Core.Organizer
|
||||
|
||||
episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList();
|
||||
|
||||
if (series.SeriesType == SeriesTypes.Daily)
|
||||
if (series.SeriesType == SeriesTypes.Daily && episodeFile.SeasonNumber > 0)
|
||||
{
|
||||
pattern = namingConfig.DailyEpisodeFormat;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,13 @@ namespace NzbDrone.Core.Tv
|
||||
|
||||
break;
|
||||
|
||||
case MonitorTypes.Pilot:
|
||||
_logger.Debug("[{0}] Monitoring first episode episodes", series.Title);
|
||||
ToggleEpisodesMonitoredState(episodes,
|
||||
e => e.SeasonNumber > 0 && e.SeasonNumber == firstSeason && e.EpisodeNumber == 1);
|
||||
|
||||
break;
|
||||
|
||||
case MonitorTypes.FirstSeason:
|
||||
_logger.Debug("[{0}] Monitoring first season episodes", series.Title);
|
||||
ToggleEpisodesMonitoredState(episodes, e => e.SeasonNumber > 0 && e.SeasonNumber == firstSeason);
|
||||
@@ -77,7 +84,6 @@ namespace NzbDrone.Core.Tv
|
||||
break;
|
||||
|
||||
case MonitorTypes.LatestSeason:
|
||||
|
||||
if (episodes.Where(e => e.SeasonNumber == lastSeason)
|
||||
.All(e => e.AirDateUtc.HasValue &&
|
||||
e.AirDateUtc.Value.Before(DateTime.UtcNow) &&
|
||||
@@ -114,7 +120,10 @@ namespace NzbDrone.Core.Tv
|
||||
// - Not specials
|
||||
// - The latest season
|
||||
// - Not only supposed to monitor the first season
|
||||
if (seasonNumber > 0 && seasonNumber == lastSeason && monitoringOptions.Monitor != MonitorTypes.FirstSeason)
|
||||
if (seasonNumber > 0 &&
|
||||
seasonNumber == lastSeason &&
|
||||
monitoringOptions.Monitor != MonitorTypes.FirstSeason &&
|
||||
monitoringOptions.Monitor != MonitorTypes.Pilot)
|
||||
{
|
||||
season.Monitored = true;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace NzbDrone.Core.Tv
|
||||
Existing,
|
||||
FirstSeason,
|
||||
LatestSeason,
|
||||
Pilot,
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,13 @@ namespace NzbDrone.Update.UpdateEngine
|
||||
|
||||
Verify(installationFolder, processId);
|
||||
|
||||
|
||||
if (installationFolder.EndsWith(@"\bin\Sonarr") || installationFolder.EndsWith(@"/bin/Sonarr"))
|
||||
{
|
||||
installationFolder = installationFolder.GetParentPath();
|
||||
_logger.Info("Fixed Installation Folder: {0}", installationFolder);
|
||||
}
|
||||
|
||||
var appType = _detectApplicationType.GetAppType();
|
||||
|
||||
_processProvider.FindProcessByName(ProcessProvider.SONARR_CONSOLE_PROCESS_NAME);
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Sonarr.Api.V3
|
||||
Get("schema", x => GetTemplates());
|
||||
Post("test", x => Test(ReadResourceFromRequest(true)));
|
||||
Post("testall", x => TestAll());
|
||||
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true)));
|
||||
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true, true)));
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetProviderById;
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Sonarr.Http.ClientSchema
|
||||
public string Type { get; set; }
|
||||
public bool Advanced { get; set; }
|
||||
public List<SelectOption> SelectOptions { get; set; }
|
||||
public string SelectOptionsProviderAction { get; set; }
|
||||
public string Section { get; set; }
|
||||
public string Hidden { get; set; }
|
||||
|
||||
|
||||
@@ -106,7 +106,14 @@ namespace Sonarr.Http.ClientSchema
|
||||
|
||||
if (fieldAttribute.Type == FieldType.Select)
|
||||
{
|
||||
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
|
||||
if (fieldAttribute.SelectOptionsProviderAction.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
field.SelectOptionsProviderAction = fieldAttribute.SelectOptionsProviderAction;
|
||||
}
|
||||
else
|
||||
{
|
||||
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldAttribute.Hidden != HiddenType.Visible)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Nancy;
|
||||
using Nancy.Responses.Negotiation;
|
||||
using Newtonsoft.Json;
|
||||
@@ -198,7 +199,7 @@ namespace Sonarr.Http.REST
|
||||
return Negotiate.WithModel(model).WithStatusCode(statusCode);
|
||||
}
|
||||
|
||||
protected TResource ReadResourceFromRequest(bool skipValidate = false)
|
||||
protected TResource ReadResourceFromRequest(bool skipValidate = false, bool skipSharedValidate = false)
|
||||
{
|
||||
TResource resource;
|
||||
|
||||
@@ -216,7 +217,12 @@ namespace Sonarr.Http.REST
|
||||
throw new BadRequestException("Request body can't be empty");
|
||||
}
|
||||
|
||||
var errors = SharedValidator.Validate(resource).Errors.ToList();
|
||||
var errors = new List<ValidationFailure>();
|
||||
|
||||
if (!skipSharedValidate)
|
||||
{
|
||||
errors.AddRange(SharedValidator.Validate(resource).Errors);
|
||||
}
|
||||
|
||||
if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user