1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-06 13:30:00 -05:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Taloth Saldono
0f4e699b57 Handle Move/Copy to same file via different mount/symlink 2020-10-03 18:46:44 +02:00
Taloth Saldono
6134aa38eb Fixed: Dataloss when moving series folder to root folder with only different casing 2020-10-03 18:46:43 +02:00
78 changed files with 3812 additions and 4622 deletions

View File

@@ -1,12 +0,0 @@
# 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']

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1.3318" y1="43.7371" x2="67.0419" y2="26.0967">
<stop offset="0.1237" style="stop-color:#7866FF"/>
<stop offset="0.5376" style="stop-color:#FE2EB6"/>
<stop offset="0.8548" style="stop-color:#FD0486"/>
</linearGradient>
<polygon style="fill:url(#SVGID_1_);" points="67.3,16 43.7,0 0,31.1 11.1,70 58.9,60.3 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="45.9148" y1="38.9098" x2="67.6577" y2="9.0989">
<stop offset="0.1237" style="stop-color:#FF0080"/>
<stop offset="0.2587" style="stop-color:#FE0385"/>
<stop offset="0.4109" style="stop-color:#FA0C92"/>
<stop offset="0.5713" style="stop-color:#F41BA9"/>
<stop offset="0.7363" style="stop-color:#EB2FC8"/>
<stop offset="0.8656" style="stop-color:#E343E6"/>
</linearGradient>
<polygon style="fill:url(#SVGID_2_);" points="67.3,16 43.7,0 38,15.7 38,47.8 70,47.8 "/>
</g>
<g>
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
<rect x="17.4" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
<g>
<path style="fill:#FFFFFF;" d="M17.4,19.1h6.9c5.6,0,9.5,3.8,9.5,8.9V28c0,5-3.9,8.9-9.5,8.9h-6.9V19.1z M21.4,22.7v10.7h3
c3.2,0,5.4-2.2,5.4-5.3V28c0-3.2-2.2-5.4-5.4-5.4H21.4z"/>
<polygon style="fill:#FFFFFF;" points="40.3,22.7 34.9,22.7 34.9,19.1 49.6,19.1 49.6,22.7 44.2,22.7 44.2,37 40.3,37 "/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve"
>
<g>
<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24">
<stop offset="0" style="stop-color:#FCEE39"/>
<stop offset="1" style="stop-color:#F37B3D"/>
</linearGradient>
<path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9
c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0
c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/>
<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546">
<stop offset="0" style="stop-color:#EF5A6B"/>
<stop offset="0.57" style="stop-color:#F26F4E"/>
<stop offset="1" style="stop-color:#F37B3D"/>
</linearGradient>
<path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0
c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3
c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/>
<linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562">
<stop offset="0" style="stop-color:#7C59A4"/>
<stop offset="0.3852" style="stop-color:#AF4C92"/>
<stop offset="0.7654" style="stop-color:#DC4183"/>
<stop offset="0.957" style="stop-color:#ED3D7D"/>
</linearGradient>
<path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9
c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0
c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/>
<linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971">
<stop offset="0" style="stop-color:#EF5A6B"/>
<stop offset="0.364" style="stop-color:#EE4E72"/>
<stop offset="1" style="stop-color:#ED3D7D"/>
</linearGradient>
<path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0
l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0
c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/>
<g id="XMLID_3008_">
<rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/>
<rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/>
<g id="XMLID_3009_">
<path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0
l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/>
<path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0
L45.3,43.8z"/>
<path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/>
<path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0
c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5
l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/>
<path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0
c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1
l-1.5,0v2H50.6z"/>
<path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z
M58.8,59l-0.9-2.3L57,59L58.8,59z"/>
<path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/>
<path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z"
/>
<path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0
c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6
c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7
C76.1,62.5,74.7,62,73.7,61.1z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<g>
<g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="22.9451" y1="75.7869" x2="74.7868" y2="20.6415">
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
<stop offset="0.4044" style="stop-color:#C41E57"/>
<stop offset="0.4677" style="stop-color:#C41E57"/>
<stop offset="0.6505" style="stop-color:#EB8523"/>
<stop offset="0.9516" style="stop-color:#FEBD11"/>
</linearGradient>
<polygon style="fill:url(#SVGID_1_);" points="49.8,15.2 36,36.7 58.4,70 70,23.1 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.7187" y1="73.2922" x2="69.5556" y2="18.1519">
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
<stop offset="0.4044" style="stop-color:#C41E57"/>
<stop offset="0.4677" style="stop-color:#C41E57"/>
<stop offset="0.7043" style="stop-color:#EB8523"/>
</linearGradient>
<polygon style="fill:url(#SVGID_2_);" points="51.1,15.7 49,0 18.8,33.6 27.6,42.3 20.8,70 58.4,70 "/>
</g>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1.8281" y1="53.4275" x2="48.8245" y2="9.2255">
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
<stop offset="0.6613" style="stop-color:#C41E57"/>
</linearGradient>
<polygon style="fill:url(#SVGID_3_);" points="49,0 11.6,0 0,47.1 55.6,47.1 "/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="49.8935" y1="-11.5569" x2="48.8588" y2="24.0352">
<stop offset="0.5" style="stop-color:#C41E57"/>
<stop offset="0.6668" style="stop-color:#D13F48"/>
<stop offset="0.7952" style="stop-color:#D94F39"/>
<stop offset="0.8656" style="stop-color:#DD5433"/>
</linearGradient>
<polygon style="fill:url(#SVGID_4_);" points="55.3,47.1 51.1,15.7 49,0 41.7,23 "/>
</g>
<g>
<rect x="13.4" y="13.5" transform="matrix(-1 2.577289e-003 -2.577289e-003 -1 70.0288 70.081)" style="fill:#000000;" width="43.2" height="43.2"/>
<rect x="17.6" y="48.6" transform="matrix(1 -2.577289e-003 2.577289e-003 1 -0.1287 6.634109e-002)" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
<path style="fill:#FFFFFF;" d="M17.4,19.1l8.2,0c2.3,0,4,0.6,5.2,1.8c1,1,1.5,2.4,1.5,4.1l0,0.1c0,1.5-0.3,2.6-1.1,3.5
c-0.7,0.9-1.6,1.6-2.8,2l4.4,6.4l-4.6,0l-3.7-5.5l-3.3,0l0,5.5l-3.9,0L17.4,19.1z M25.3,27.8c1,0,1.7-0.2,2.2-0.7
c0.5-0.5,0.8-1.1,0.8-1.8l0-0.1c0-0.9-0.3-1.5-0.8-1.9c-0.5-0.4-1.3-0.6-2.3-0.6l-3.9,0l0,5.1L25.3,27.8z"/>
<path style="fill:#FFFFFF;" d="M36,33.2l-1.9,0l0-3.3l2.5,0l0.6-3.8l-2.3,0l0-3.3l2.8,0l0.6-3.7l3.4,0l-0.6,3.7l3.7,0l0.6-3.7
l3.4,0l-0.6,3.7l1.9,0l0,3.3l-2.5,0L47,29.9l2.3,0l0,3.3l-2.8,0L45.8,37l-3.4,0l0.7-3.8l-3.7,0L38.7,37l-3.4,0L36,33.2z
M43.7,29.9l0.6-3.8l-3.7,0L40,29.9L43.7,29.9z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 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>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -66,27 +66,8 @@ 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
### 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
[![Sponsors](https://opencollective.com/sonarr/tiers/sponsor.svg)](https://opencollective.com/sonarr/contribute/sponsor-21443/checkout)
#### Flexible Sponsors
[![Flexible Sponsors](https://opencollective.com/sonarr/tiers/flexible-sponsor.svg?avatarHeight=54)](https://opencollective.com/sonarr/contribute/flexible-sponsor-21457/checkout)
#### Backers
[![Backers](https://opencollective.com/sonarr/tiers/backer.svg?avatarHeight=48)](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/)
### Sponsors
- [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/)

View File

@@ -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 as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts)"; 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,8 +75,7 @@ function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ResultCode: Integer;
begin
Exec('net', 'stop nzbdrone', '', 0, ewWaitUntilTerminated, ResultCode)
Exec('sc', 'delete nzbdrone', '', 0, ewWaitUntilTerminated, ResultCode)
Exec(ExpandConstant('{commonappdata}\NzbDrone\bin\NzbDrone.Console.exe'), '/u', '', 0, ewWaitUntilTerminated, ResultCode)
end;
function Framework472IsNotInstalled(): Boolean;

View File

@@ -6,7 +6,6 @@ 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';
@@ -14,7 +13,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 = 'no-fallback';
const inlineWebWorkers = true;
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
@@ -32,19 +31,14 @@ 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.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 head = assetTags.head.map((v) => {
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` };
return this.createHtmlTag(v);
});
const body = assetTags.bodyTags.map((v) => {
const body = assetTags.body.map((v) => {
v.attributes = { src: `/${v.attributes.src}` };
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
return this.createHtmlTag(v);
});
return html
@@ -128,8 +122,9 @@ const config = {
use: {
loader: 'worker-loader',
options: {
filename: '[name].js',
inline: inlineWebWorkers
name: '[name].js',
inline: inlineWebWorkers,
fallback: !inlineWebWorkers
}
}
},

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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, defaultState } from 'Store/Actions/providerOptionActions';
import { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions';
import DeviceInput from './DeviceInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.providerOptions.devices || defaultState,
(state) => state.providerOptions,
(value, devices) => {
return {
@@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
}
componentWillUnmount = () => {
this.props.dispatchClearOptions({ section: 'devices' });
this.props.dispatchClearOptions();
}
//
@@ -65,7 +65,6 @@ class DeviceInputConnector extends Component {
} = this.props;
dispatchFetchOptions({
section: 'devices',
action: 'getDevices',
provider,
providerData

View File

@@ -66,8 +66,3 @@
border-radius: 4px;
background-color: $white;
}
.loading {
display: inline-block;
margin: 5px -5px 5px 0;
}

View File

@@ -10,7 +10,6 @@ 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';
@@ -251,10 +250,6 @@ class EnhancedSelectInput extends Component {
this._addListener();
}
if (!this.state.isOpen && this.props.onOpen) {
this.props.onOpen();
}
this.setState({ isOpen: !this.state.isOpen });
}
@@ -300,7 +295,6 @@ class EnhancedSelectInput extends Component {
value,
values,
isDisabled,
isFetching,
hasError,
hasWarning,
valueOptions,
@@ -361,21 +355,9 @@ class EnhancedSelectInput extends Component {
styles.dropdownArrowContainer
}
>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</Measure>
@@ -501,14 +483,12 @@ 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
};
@@ -516,7 +496,6 @@ EnhancedSelectInput.defaultProps = {
className: styles.enhancedSelect,
disabledClassName: styles.isDisabled,
isDisabled: false,
isFetching: false,
valueOptions: {},
selectedValueOptions: {},
selectedValueComponent: HintedSelectInputSelectedValue,

View File

@@ -1,159 +0,0 @@
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);

View File

@@ -20,7 +20,7 @@
.optionCheckInput {
composes: input from '~./CheckInput.css';
margin-top: 0;
margin-top: 0px;
}
.isSelected {

View File

@@ -32,7 +32,6 @@ class EnhancedSelectInputOption extends Component {
const {
className,
id,
depth,
isSelected,
isDisabled,
isHidden,
@@ -55,11 +54,6 @@ class EnhancedSelectInputOption extends Component {
onPress={this.onPress}
>
{
depth !== 0 &&
<div style={{ width: `${depth * 20}px` }} />
}
{
isMultiSelect &&
<CheckInput
@@ -90,7 +84,6 @@ 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,
@@ -102,7 +95,6 @@ EnhancedSelectInputOption.propTypes = {
EnhancedSelectInputOption.defaultProps = {
className: styles.option,
depth: 0,
isDisabled: false,
isHidden: false,
isMultiSelect: false

View File

@@ -18,7 +18,6 @@ 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';
@@ -72,9 +71,6 @@ function getComponent(type) {
case inputTypes.SELECT:
return EnhancedSelectInput;
case inputTypes.DYNAMIC_SELECT:
return EnhancedSelectInputConnector;
case inputTypes.SERIES_TYPE_SELECT:
return SeriesTypeSelectInput;

View File

@@ -9,7 +9,6 @@ function HintedSelectInputOption(props) {
id,
value,
hint,
depth,
isSelected,
isDisabled,
isMultiSelect,
@@ -20,7 +19,6 @@ function HintedSelectInputOption(props) {
return (
<EnhancedSelectInputOption
id={id}
depth={depth}
isSelected={isSelected}
isDisabled={isDisabled}
isHidden={isDisabled}
@@ -50,7 +48,6 @@ 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,

View File

@@ -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, selectOptionsProviderAction }) {
function getType(type, value) {
switch (type) {
case 'captcha':
return inputTypes.CAPTCHA;
@@ -23,9 +23,6 @@ function getType({ type, selectOptionsProviderAction }) {
case 'filePath':
return inputTypes.PATH;
case 'select':
if (selectOptionsProviderAction) {
return inputTypes.DYNAMIC_SELECT;
}
return inputTypes.SELECT;
case 'tag':
return inputTypes.TEXT_TAG;
@@ -88,7 +85,7 @@ function ProviderFieldFormGroup(props) {
<FormLabel>{label}</FormLabel>
<FormInputGroup
type={getType(props)}
type={getType(type, value)}
name={name}
label={label}
helpText={helpText}
@@ -108,8 +105,7 @@ function ProviderFieldFormGroup(props) {
const selectOptionsShape = {
name: PropTypes.string.isRequired,
value: PropTypes.number.isRequired,
hint: PropTypes.string
value: PropTypes.number.isRequired
};
ProviderFieldFormGroup.propTypes = {
@@ -126,7 +122,6 @@ 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
};

View File

@@ -61,7 +61,7 @@ class FilterMenuContent extends Component {
{
showCustomFilters &&
<MenuItem onPress={onCustomFiltersPress}>
Custom Filters
Custom Filters
</MenuItem>
}
</MenuContent>

View File

@@ -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> :

View File

@@ -156,35 +156,3 @@
.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);
}
} */

View File

@@ -5,25 +5,8 @@ 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 {
//
@@ -34,7 +17,6 @@ class Tooltip extends Component {
this._scheduleUpdate = null;
this._closeTimeout = null;
this._maxWidth = maxWidth || getMaxWidth();
this.state = {
isOpen: false
@@ -72,11 +54,9 @@ 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 = Math.min(this._maxWidth, windowWidth - right - 20);
data.styles.maxHeight = top - 20;
data.styles.maxWidth = windowWidth - right - 50;
} else {
data.styles.maxWidth = Math.min(this._maxWidth, left - 20);
data.styles.maxHeight = top - 20;
data.styles.maxWidth = left - 35;
}
return data;
@@ -164,16 +144,10 @@ 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={classNames(
styles.tooltipContainer,
vertical ? styles.verticalContainer : styles.horizontalContainer
)}
className={styles.tooltipContainer}
style={style}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
@@ -182,7 +156,7 @@ class Tooltip extends Component {
className={this.state.isOpen ? classNames(
styles.arrow,
styles[kind],
styles[popperPlacement]
styles[placement.split('-')[0]]
) : styles.arrowDisabled}
ref={arrowProps.ref}
style={arrowProps.style}
@@ -227,7 +201,7 @@ Tooltip.defaultProps = {
bodyClassName: styles.body,
kind: kinds.DEFAULT,
position: tooltipPositions.TOP,
canFlip: false
canFlip: true
};
export default Tooltip;

View File

@@ -13,7 +13,6 @@ 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';
@@ -35,7 +34,6 @@ export const all = [
INDEXER_SELECT,
ROOT_FOLDER_SELECT,
SELECT,
DYNAMIC_SELECT,
SERIES_TYPE_SELECT,
TAG,
TEXT,

View File

@@ -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
}

View File

@@ -101,8 +101,7 @@ class SeriesIndexOverview extends Component {
seasonCount,
episodeCount,
episodeFileCount,
totalEpisodeCount,
sizeOnDisk
totalEpisodeCount
} = statistics;
const {
@@ -213,7 +212,6 @@ class SeriesIndexOverview extends Component {
nextAiring={nextAiring}
seasonCount={seasonCount}
qualityProfile={qualityProfile}
sizeOnDisk={sizeOnDisk}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}

View File

@@ -15,6 +15,7 @@ const rows = [
name: 'monitored',
showProp: 'showMonitored',
valueProp: 'monitored'
},
{
name: 'network',

View File

@@ -71,8 +71,7 @@ class SeriesIndexOverviews extends Component {
items,
sortKey,
overviewOptions,
jumpToCharacter,
isSmallScreen
jumpToCharacter
} = this.props;
const {
@@ -82,17 +81,13 @@ class SeriesIndexOverviews extends Component {
if (prevProps.sortKey !== sortKey ||
prevProps.overviewOptions !== overviewOptions) {
this.calculateGrid(this.state.width, isSmallScreen);
this.calculateGrid();
}
if (
this._grid &&
if (this._grid &&
(prevState.width !== width ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items) ||
prevProps.overviewOptions !== overviewOptions
)
) {
hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}

View File

@@ -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>
);

View File

@@ -140,7 +140,7 @@ EditRemotePathMappingModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.shape(remotePathMappingShape).isRequired,
downloadClientHosts: PropTypes.arrayOf(PropTypes.object).isRequired,
downloadClientHosts: PropTypes.arrayOf(PropTypes.string).isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,

View File

@@ -161,7 +161,7 @@ function EditImportListModalContent(props) {
<FormGroup>
<FormLabel>
Series Type
Series Type
<Popover
anchor={

View File

@@ -18,6 +18,7 @@
}
.footNote {
display: flex;
color: $helpTextColor;

View File

@@ -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>

View File

@@ -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';

View File

@@ -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

View File

@@ -24,7 +24,7 @@
height: 20px;
}
.track {
.bar {
top: 9px;
margin: 0 5px;
height: 3px;
@@ -36,7 +36,7 @@
}
}
.thumb {
.handle {
top: 1px;
z-index: 0 !important;
width: 18px;

View File

@@ -68,27 +68,6 @@ class QualityDefinition extends Component {
}
}
//
// Control
trackRenderer(props, state) {
return (
<div
{...props}
className={styles.track}
/>
);
}
thumbRenderer(props, state) {
return (
<div
{...props}
className={styles.thumb}
/>
);
}
//
// Listeners
@@ -183,16 +162,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]}
withTracks={true}
withBars={true}
snapDragDisabled={true}
renderThumb={this.thumbRenderer}
renderTrack={this.trackRenderer}
className={styles.slider}
barClassName={styles.bar}
handleClassName={styles.handle}
onChange={this.onSliderChange}
onAfterChange={this.onAfterSliderChange}
/>

View File

@@ -68,17 +68,6 @@ 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"

View File

@@ -167,7 +167,7 @@ function TagDetailsModalContent(props) {
isDisabled={isTagUsed}
onPress={onDeleteTagPress}
>
Delete
Delete
</Button>
}

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import { createAction } from 'redux-actions';
import requestAction from 'Utilities/requestAction';
import updateSectionState from 'Utilities/State/updateSectionState';
@@ -11,9 +10,6 @@ import { set } from './baseActions';
export const section = 'providerOptions';
const lastActions = {};
let lastActionId = 0;
//
// State
@@ -27,8 +23,8 @@ export const defaultState = {
//
// Actions Types
export const FETCH_OPTIONS = 'providers/fetchOptions';
export const CLEAR_OPTIONS = 'providers/clearOptions';
export const FETCH_OPTIONS = 'devices/fetchOptions';
export const CLEAR_OPTIONS = 'devices/clearOptions';
//
// Action Creators
@@ -42,55 +38,30 @@ 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: subsection,
section,
isFetching: true
}));
const promise = requestAction(payload);
promise.done((data) => {
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 || []
}));
}
dispatch(set({
section,
isFetching: false,
isPopulated: true,
error: null,
items: data.options || []
}));
});
promise.fail((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
}));
}
dispatch(set({
section,
isFetching: false,
isPopulated: false,
error: xhr
}));
});
}
});
@@ -100,12 +71,8 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({
[CLEAR_OPTIONS]: function(state, { payload }) {
const subsection = `${section}.${payload.section}`;
lastActions[payload.section] = null;
return updateSectionState(state, subsection, defaultState);
[CLEAR_OPTIONS]: function(state) {
return updateSectionState(state, section, defaultState);
}
}, {}, section);
}, defaultState, section);

View File

@@ -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>

View File

@@ -82,7 +82,7 @@ function LogsTable(props) {
{
isPopulated && !error && !items.length &&
<div>
No events found
No events found
</div>
}

View File

@@ -3,7 +3,6 @@ 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' }

View File

@@ -15,69 +15,69 @@
"license": "GPL-3.0",
"readmeFilename": "readme.md",
"dependencies": {
"@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",
"@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",
"ansi-colors": "4.1.1",
"autoprefixer": "9.6.1",
"babel-eslint": "10.1.0",
"babel-loader": "8.1.0",
"babel-eslint": "10.0.2",
"babel-loader": "8.0.6",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"classnames": "2.2.6",
"clipboard": "2.0.6",
"connected-react-router": "6.8.0",
"core-js": "3.6.5",
"clipboard": "2.0.4",
"connected-react-router": "6.5.2",
"core-js": "3",
"create-react-class": "15.6.3",
"css-loader": "3.0.0",
"del": "6.0.0",
"del": "5.0.0",
"element-class": "0.2.2",
"esformatter": "0.11.3",
"eslint": "7.10.0",
"esformatter": "0.10.0",
"eslint": "6.0.1",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-react": "7.21.3",
"esprint": "0.7.0",
"file-loader": "6.1.0",
"filesize": "6.1.0",
"fuse.js": "6.4.1",
"eslint-plugin-react": "7.14.2",
"esprint": "0.4.0",
"file-loader": "4.0.0",
"filesize": "4.1.2",
"fuse.js": "3.4.5",
"gulp": "4.0.2",
"gulp-cached": "1.1.1",
"gulp-concat": "2.6.1",
"gulp-declare": "0.3.0",
"gulp-livereload": "4.0.2",
"gulp-livereload": "4.0.1",
"gulp-postcss": "8.0.0",
"gulp-print": "5.0.2",
"gulp-sourcemaps": "2.6.5",
"gulp-stripbom": "1.0.5",
"gulp-stripbom": "1.0.4",
"gulp-watch": "5.0.1",
"gulp-wrap": "0.15.0",
"history": "4.9.0",
"html-webpack-plugin": "4.5.0",
"html-webpack-plugin": "3.2.0",
"jdu": "1.0.0",
"jquery": "3.5.1",
"loader-utils": "^2.0.0",
"lodash": "4.17.20",
"jquery": "3.4.1",
"loader-utils": "^1.1.0",
"lodash": "4.17.14",
"mini-css-extract-plugin": "0.8.0",
"mobile-detect": "1.4.4",
"moment": "2.29.0",
"mousetrap": "1.6.5",
"mobile-detect": "1.4.3",
"moment": "2.24.0",
"mousetrap": "1.6.3",
"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.9.4",
"react": "16.13.1",
"qs": "6.7.0",
"react": "16.8.6",
"react-addons-shallow-compare": "15.6.2",
"react-async-script": "1.2.0",
"react-autosuggest": "10.0.2",
"react-async-script": "1.1.1",
"react-autosuggest": "9.4.3",
"react-custom-scrollbars": "4.2.1",
"react-dnd": "11.1.3",
"react-dnd-html5-backend": "11.1.3",
"react-dnd": "9.3.2",
"react-dnd-html5-backend": "9.3.2",
"react-document-title": "2.0.3",
"react-dom": "16.13.1",
"react-focus-lock": "2.4.1",
"react-google-recaptcha": "2.1.0",
"react-lazyload": "3.0.0",
"react-dom": "16.8.6",
"react-focus-lock": "2.2.1",
"react-google-recaptcha": "1.1.0",
"react-lazyload": "2.6.2",
"react-measure": "1.4.7",
"react-popper": "1.3.3",
"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-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-virtualized": "9.21.1",
"redux": "4.0.5",
"redux": "4.0.4",
"redux-actions": "2.6.5",
"redux-batched-actions": "0.5.0",
"redux-batched-actions": "0.4.1",
"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": "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"
"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"
},
"main": "index.js",
"browserslist": [

View File

@@ -402,58 +402,6 @@ 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()
{
@@ -506,8 +454,6 @@ namespace NzbDrone.Common.Test.DiskTests
[Test]
public void MoveFolder_should_detect_caseinsensitive_parents()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
@@ -523,8 +469,6 @@ namespace NzbDrone.Common.Test.DiskTests
[Test]
public void MoveFolder_should_rename_caseinsensitive_folder()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
@@ -539,26 +483,6 @@ 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()
{
@@ -661,23 +585,6 @@ 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()
{

View File

@@ -20,7 +20,6 @@ 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

View File

@@ -8,8 +8,6 @@ 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);

View File

@@ -1,9 +1,12 @@
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
@@ -29,16 +32,13 @@ namespace NzbDrone.Common.Disk
private string ResolveRealParentPath(string path)
{
var parentPath = path.GetParentPath();
if (!_diskProvider.FolderExists(parentPath))
if (!_diskProvider.FolderExists(path))
{
return path;
}
var realParentPath = parentPath.GetActualCasing();
var partialChildPath = path.Substring(parentPath.Length);
return realParentPath + partialChildPath;
parentPath = parentPath.GetActualCasing();
return parentPath + Path.DirectorySeparatorChar + Path.GetFileName(path);
}
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
@@ -245,15 +245,17 @@ 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));
@@ -266,7 +268,7 @@ namespace NzbDrone.Common.Disk
_diskProvider.MoveFile(sourcePath, tempPath, true);
try
{
ClearTargetPath(sourcePath, targetPath, overwrite);
ClearTargetPath(targetPath, overwrite);
_diskProvider.MoveFile(tempPath, targetPath);
@@ -296,7 +298,77 @@ namespace NzbDrone.Common.Disk
throw new IOException(string.Format("Destination cannot be a child of the source [{0}] => [{1}]", sourcePath, targetPath));
}
ClearTargetPath(sourcePath, targetPath, overwrite);
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);
}
}
if (mode.HasFlag(TransferMode.HardLink))
{
@@ -322,7 +394,7 @@ namespace NzbDrone.Common.Disk
var isCifs = targetDriveFormat == "cifs";
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
if (mode.HasFlag(TransferMode.Copy))
{
if (isBtrfs)
@@ -363,7 +435,7 @@ namespace NzbDrone.Common.Disk
_diskProvider.DeleteFile(sourcePath);
return TransferMode.Move;
}
TryMoveFileVerified(sourcePath, targetPath, originalSize);
return TransferMode.Move;
}
@@ -371,7 +443,7 @@ namespace NzbDrone.Common.Disk
return TransferMode.None;
}
private void ClearTargetPath(string sourcePath, string targetPath, bool overwrite)
private void ClearTargetPath(string targetPath, bool overwrite)
{
if (_diskProvider.FileExists(targetPath))
{

View File

@@ -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;

View File

@@ -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)))

View File

@@ -288,7 +288,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
}
[Test]
public void should_use_airDate_if_series_isDaily_and_not_a_special()
public void should_use_airDate_if_series_isDaily()
{
_namingConfig.DailyEpisodeFormat = "{Series Title} - {air-date} - {Episode Title}";
@@ -297,34 +297,11 @@ 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()
{
@@ -335,10 +312,6 @@ 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");

View File

@@ -19,7 +19,6 @@ 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; }
@@ -39,15 +38,6 @@ 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,

View File

@@ -70,23 +70,14 @@ 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);
}
@@ -235,23 +226,13 @@ 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();
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;
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
}
private List<DownloadDecision> SearchAnimeSeason(Series series, List<Episode> episodes, bool userInvokedSearch, bool interactiveSearch)

View File

@@ -332,14 +332,7 @@ namespace NzbDrone.Core.Indexers
{
var parser = GetParser();
var generator = GetRequestGenerator();
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);
var releases = FetchPage(generator.GetRecentRequests().GetAllTiers().First().First(), parser);
if (releases.Empty())
{

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
@@ -134,31 +133,5 @@ 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);
}
}
}

View File

@@ -8,7 +8,6 @@ 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
{
@@ -50,7 +49,6 @@ namespace NzbDrone.Core.Indexers.Newznab
}
var request = new HttpRequest(url, HttpAccept.Rss);
request.AllowAutoRedirect = true;
HttpResponse response;

View File

@@ -1,75 +0,0 @@
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;
}
}
}

View File

@@ -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", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows")]
[FieldDefinition(3, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows")]
public IEnumerable<int> Categories { get; set; }
[FieldDefinition(4, Label = "Anime Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Comma Separated list, leave blank to disable anime")]
[FieldDefinition(4, Label = "Anime Categories", 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)]

View File

@@ -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,28 +124,5 @@ 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);
}
}
}

View File

@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Notifications.Webhook
var payload = new WebhookGrabPayload
{
EventType = WebhookEventType.Grab,
EventType = "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 = WebhookEventType.Download,
EventType = "Download",
Series = new WebhookSeries(message.Series),
Episodes = episodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x)),
EpisodeFile = new WebhookEpisodeFile(episodeFile),
@@ -67,29 +67,15 @@ namespace NzbDrone.Core.Notifications.Webhook
public override void OnRename(Series series)
{
var payload = new WebhookRenamePayload
var payload = new WebhookPayload
{
EventType = WebhookEventType.Rename,
EventType = "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()
@@ -107,7 +93,7 @@ namespace NzbDrone.Core.Notifications.Webhook
{
var payload = new WebhookGrabPayload
{
EventType = WebhookEventType.Test,
EventType = "Test",
Series = new WebhookSeries()
{
Id = 1,

View File

@@ -15,7 +15,6 @@ namespace NzbDrone.Core.Notifications.Webhook
QualityVersion = episodeFile.Quality.Revision.Version;
ReleaseGroup = episodeFile.ReleaseGroup;
SceneName = episodeFile.SceneName;
Size = episodeFile.Size;
}
public int Id { get; set; }
@@ -25,6 +24,5 @@ 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; }
}
}

View File

@@ -1,17 +0,0 @@
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
}
}

View File

@@ -4,7 +4,6 @@ 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; }

View File

@@ -1,12 +0,0 @@
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; }
}
}

View File

@@ -4,7 +4,6 @@ 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; }

View File

@@ -2,6 +2,7 @@
{
public class WebhookPayload
{
public WebhookEventType EventType { get; set; }
public string EventType { get; set; }
public WebhookSeries Series { get; set; }
}
}

View File

@@ -1,7 +0,0 @@
namespace NzbDrone.Core.Notifications.Webhook
{
public class WebhookRenamePayload : WebhookPayload
{
public WebhookSeries Series { get; set; }
}
}

View File

@@ -131,7 +131,7 @@ namespace NzbDrone.Core.Organizer
episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList();
if (series.SeriesType == SeriesTypes.Daily && episodeFile.SeasonNumber > 0)
if (series.SeriesType == SeriesTypes.Daily)
{
pattern = namingConfig.DailyEpisodeFormat;
}

View File

@@ -70,13 +70,6 @@ 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);
@@ -84,6 +77,7 @@ 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) &&
@@ -120,10 +114,7 @@ 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 &&
monitoringOptions.Monitor != MonitorTypes.Pilot)
if (seasonNumber > 0 && seasonNumber == lastSeason && monitoringOptions.Monitor != MonitorTypes.FirstSeason)
{
season.Monitored = true;
}

View File

@@ -18,7 +18,6 @@ namespace NzbDrone.Core.Tv
Existing,
FirstSeason,
LatestSeason,
Pilot,
None
}
}

View File

@@ -84,13 +84,6 @@ 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);

View File

@@ -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, true)));
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true)));
GetResourceAll = GetAll;
GetResourceById = GetProviderById;

View File

@@ -14,7 +14,6 @@ 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; }

View File

@@ -106,14 +106,7 @@ namespace Sonarr.Http.ClientSchema
if (fieldAttribute.Type == FieldType.Select)
{
if (fieldAttribute.SelectOptionsProviderAction.IsNotNullOrWhiteSpace())
{
field.SelectOptionsProviderAction = fieldAttribute.SelectOptionsProviderAction;
}
else
{
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
}
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
}
if (fieldAttribute.Hidden != HiddenType.Visible)

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using Nancy;
using Nancy.Responses.Negotiation;
using Newtonsoft.Json;
@@ -199,7 +198,7 @@ namespace Sonarr.Http.REST
return Negotiate.WithModel(model).WithStatusCode(statusCode);
}
protected TResource ReadResourceFromRequest(bool skipValidate = false, bool skipSharedValidate = false)
protected TResource ReadResourceFromRequest(bool skipValidate = false)
{
TResource resource;
@@ -217,12 +216,7 @@ namespace Sonarr.Http.REST
throw new BadRequestException("Request body can't be empty");
}
var errors = new List<ValidationFailure>();
if (!skipSharedValidate)
{
errors.AddRange(SharedValidator.Validate(resource).Errors);
}
var errors = SharedValidator.Validate(resource).Errors.ToList();
if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase))
{

7009
yarn.lock

File diff suppressed because it is too large Load Diff