diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 6bd416a38..a6c3f5fd1 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,5 +1,41 @@
+
+## Support / Questions
+Please use https://forums.sonarr.tv/ for support. Support requests or questions will be redirected to the forums and the issue will be closed.
-Provide a description of the feature request or bug, the more details the better.
-Please use https://forums.sonarr.tv/ for support or other questions. (When in doubt, use the forums)
+
+
+## Bug Report
+
+### System Information/Logs
+
+**Sonarr Version:**
+
+**Operating System:**
+
+**.net Framework (Windows) or mono (macOS/Linux) Version:**
+
+**Link to Log Files (debug or trace):**
+
+**Browser (for UI bugs):**
+
+### Additional Information
+
+
+
+## Feature Request
+
+### What problem are you looking to solve?
+
+### Other Information
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..da40b7d70
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,28 @@
+---
+name: Bug report
+about: Create a report to help us improve Sonarr
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Logs**
+Link to debug or trace log files.
+
+**System Information**
+
+ - Sonarr Version: [e.g. 2.0.0.1]
+ - Operating System: [e.g. Windows 10]
+ - .net Framework (Windows) or mono (macOS/Linux) Version: [e.g. 4.5 or 5.12]
+
+**UI Bugs:**
+ - OS: [e.g. Windows]
+ - Browser: [e.g. chrome, firefox]
+ - Version: [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..eb428e271
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,14 @@
+---
+name: Feature request
+about: Suggest an idea for Sonarr
+
+---
+
+**Describe the problem**
+A clear and concise description of the problem you're looking to solve.
+
+**Describe any solutions you think might work**
+A clear and concise description of any solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/other-issues.md b/.github/ISSUE_TEMPLATE/other-issues.md
new file mode 100644
index 000000000..16c3ba22c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/other-issues.md
@@ -0,0 +1,7 @@
+---
+name: Other issues
+about: How to get support or ask questions
+
+---
+
+Please use https://forums.sonarr.tv/ for support. Support requests or questions will be redirected to the forums and the issue will be closed.
diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md
new file mode 100644
index 000000000..c6cf56c79
--- /dev/null
+++ b/.github/SUPPORT.md
@@ -0,0 +1,7 @@
+## Support
+
+There are a number of frequently asked questions that have been answered in our [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
+
+The [wiki](https://github.com/Sonarr/Sonarr/wiki) contains other information and guides
+
+If you have a support question, please use the [support forums](https://forums.sonarr.tv/).
diff --git a/.gitignore b/.gitignore
index 8762d35b3..bd5b618c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,6 +130,7 @@ output/*
#OS X metadata files
._*
+.DS_Store
_start
_temp_*/**/*
diff --git a/COPYRIGHT.md b/COPYRIGHT.md
new file mode 100644
index 000000000..c9c7745bb
--- /dev/null
+++ b/COPYRIGHT.md
@@ -0,0 +1,11 @@
+Copyright (C)
+2014-2017 Mark McDowall, Keivan Beigi, Taloth Saldono and contributors
+2010-2014 Mark McDowall, Keivan Beigi and contributors
+
+_Please refer to the git commit log for details on all contributors and their respective contributions._
+
+This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You can find a copy of the GNU General Public License in the LICENSE.md file and .
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 000000000..2a99aeee3
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,675 @@
+### GNU GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+### Preamble
+
+The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom
+to share and change all versions of a program--to make sure it remains
+free software for all its users. We, the Free Software Foundation, use
+the GNU General Public License for most of our software; it applies
+also to any other work released this way by its authors. You can apply
+it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you
+have certain responsibilities if you distribute copies of the
+software, or if you modify it: responsibilities to respect the freedom
+of others.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the
+manufacturer can do so. This is fundamentally incompatible with the
+aim of protecting users' freedom to change the software. The
+systematic pattern of such abuse occurs in the area of products for
+individuals to use, which is precisely where it is most unacceptable.
+Therefore, we have designed this version of the GPL to prohibit the
+practice for those products. If such problems arise substantially in
+other domains, we stand ready to extend this provision to those
+domains in future versions of the GPL, as needed to protect the
+freedom of users.
+
+Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish
+to avoid the special danger that patents applied to a free program
+could make it effectively proprietary. To prevent this, the GPL
+assures that patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS
+
+#### 0. Definitions.
+
+"This License" refers to version 3 of the GNU General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+#### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+#### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+#### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+#### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+#### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+#### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+#### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+#### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+#### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+#### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+#### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+#### 13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in
+detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU General Public
+License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that numbered version or
+of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of the GNU General Public
+License, you may choose any version ever published by the Free
+Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU General Public License can be used, that proxy's public
+statement of acceptance of a version permanently authorizes you to
+choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+#### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+#### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+#### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+### How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+To do so, attach the following notices to the program. It is safest to
+attach them to the start of each source file to most effectively state
+the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper
+mail.
+
+If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands \`show w' and \`show c' should show the
+appropriate parts of the General Public License. Of course, your
+program's commands might be different; for a GUI interface, you would
+use an "about box".
+
+You should also get your employer (if you work as a programmer) or
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. For more information on this, and how to apply and follow
+the GNU GPL, see .
+
+The GNU General Public License does not permit incorporating your
+program into proprietary programs. If your program is a subroutine
+library, you may consider it more useful to permit linking proprietary
+applications with the library. If this is what you want to do, use the
+GNU Lesser General Public License instead of this License. But first,
+please read .
\ No newline at end of file
diff --git a/gulp/copy.js b/gulp/copy.js
index ab380855d..9962defef 100644
--- a/gulp/copy.js
+++ b/gulp/copy.js
@@ -25,7 +25,7 @@ gulp.task('copyHtml', function () {
});
gulp.task('copyContent', function () {
- return gulp.src([paths.src.content + '**/*.*', '!**/*.less'])
+ return gulp.src([paths.src.content + '**/*.*', '!**/*.less', '!**/*.css'])
.pipe(gulp.dest(paths.dest.content))
.pipe(livereload());
});
diff --git a/gulp/less.js b/gulp/less.js
index 76e04b8dc..46ea2f310 100644
--- a/gulp/less.js
+++ b/gulp/less.js
@@ -5,7 +5,7 @@ var postcss = require('gulp-postcss');
var sourcemaps = require('gulp-sourcemaps');
var autoprefixer = require('autoprefixer-core');
var livereload = require('gulp-livereload');
-
+var cleancss = require('gulp-clean-css');
var print = require('gulp-print');
var paths = require('./paths');
var errorHandler = require('./errorHandler');
@@ -16,6 +16,10 @@ gulp.task('less', function() {
paths.src.content + 'bootstrap.less',
paths.src.content + 'theme.less',
paths.src.content + 'overrides.less',
+ paths.src.content + 'bootstrap.toggle-switch.css',
+ paths.src.content + 'fullcalendar.css',
+ paths.src.content + 'Messenger/messenger.css',
+ paths.src.content + 'Messenger/messenger.flat.css',
paths.src.root + 'Series/series.less',
paths.src.root + 'Activity/activity.less',
paths.src.root + 'AddSeries/addSeries.less',
@@ -33,12 +37,13 @@ gulp.task('less', function() {
.pipe(sourcemaps.init())
.pipe(less({
dumpLineNumbers : 'false',
- compress : true,
- yuicompress : true,
+ compress : false,
+ yuicompress : false,
ieCompat : true,
strictImports : true
}))
.pipe(postcss([ autoprefixer({ browsers: ['last 2 versions'] }) ]))
+ .pipe(cleancss())
.on('error', errorHandler.onError)
.pipe(sourcemaps.write(paths.dest.content))
.pipe(gulp.dest(paths.dest.content))
diff --git a/package.json b/package.json
index c3556ed7f..3d720403f 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"del": "1.2.0",
"gulp": "3.9.0",
"gulp-cached": "1.1.0",
+ "gulp-clean-css": "^3.0.4",
"gulp-concat": "2.6.0",
"gulp-declare": "0.3.0",
"gulp-handlebars": "3.0.1",
diff --git a/setup/build.bat b/setup/build.bat
index 1821e5844..964fe8abf 100644
--- a/setup/build.bat
+++ b/setup/build.bat
@@ -1,3 +1,3 @@
-#SET BUILD_NUMBER=1
-#SET branch=develop
+REM SET BUILD_NUMBER=1
+REM SET branch=develop
inno\ISCC.exe nzbdrone.iss
\ No newline at end of file
diff --git a/setup/nzbdrone.iss b/setup/nzbdrone.iss
index e667c0d03..3e471e100 100644
--- a/setup/nzbdrone.iss
+++ b/setup/nzbdrone.iss
@@ -40,8 +40,11 @@ VersionInfoVersion={#BuildNumber}
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
-;Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-Name: "windowsService"; Description: "Install as a Windows Service"
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"
+Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts)"; GroupDescription: "Start automatically"; Flags: exclusive
+Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
+Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
+
[Files]
Source: "..\_output\NzbDrone.exe"; DestDir: "{app}"; Flags: ignoreversion
@@ -51,10 +54,13 @@ Source: "..\_output\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs cr
[Icons]
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"
+Name: "{userstartup}\{#AppName}"; Filename: "{app}\NzbDrone.exe"; WorkingDir: "{app}"; Tasks: startupShortcut
[Run]
-Filename: "{app}\nzbdrone.console.exe"; Parameters: "/u"; Flags: waituntilterminated;
-Filename: "{app}\nzbdrone.console.exe"; Parameters: "/i"; Flags: waituntilterminated; Tasks: windowsService
+Filename: "{app}\nzbdrone.console.exe"; Parameters: "/u"; Flags: runhidden waituntilterminated;
+Filename: "{app}\nzbdrone.console.exe"; Parameters: "/i"; Flags: runhidden waituntilterminated; Tasks: windowsService
+Filename: "{app}\NzbDrone.exe"; Description: "Open Sonarr"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
+Filename: "{app}\NzbDrone.exe"; Description: "Start Sonarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
[UninstallRun]
Filename: "{app}\nzbdrone.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
diff --git a/src/Libraries/MediaInfo/MediaInfo.dll b/src/Libraries/MediaInfo/MediaInfo.dll
index 6a0b4f40f..24e6cb986 100644
Binary files a/src/Libraries/MediaInfo/MediaInfo.dll and b/src/Libraries/MediaInfo/MediaInfo.dll differ
diff --git a/src/Libraries/MediaInfo/libmediainfo.0.dylib b/src/Libraries/MediaInfo/libmediainfo.0.dylib
index 9aac5caea..5e5383ded 100644
Binary files a/src/Libraries/MediaInfo/libmediainfo.0.dylib and b/src/Libraries/MediaInfo/libmediainfo.0.dylib differ
diff --git a/src/LogentriesNLog/LogentriesNLog.csproj b/src/LogentriesNLog/LogentriesNLog.csproj
index 54bf715e7..fba6880c2 100644
--- a/src/LogentriesNLog/LogentriesNLog.csproj
+++ b/src/LogentriesNLog/LogentriesNLog.csproj
@@ -52,10 +52,14 @@
- ..\packages\NLog.4.4.3\lib\net40\NLog.dll
+ ..\packages\NLog.4.5.3\lib\net40-client\NLog.dll
+
+
+
+
diff --git a/src/LogentriesNLog/packages.config b/src/LogentriesNLog/packages.config
index a14101dce..6f6ef792a 100644
--- a/src/LogentriesNLog/packages.config
+++ b/src/LogentriesNLog/packages.config
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/src/Marr.Data/Reflection/SimpleReflectionStrategy.cs b/src/Marr.Data/Reflection/SimpleReflectionStrategy.cs
index 9e741b013..25b25de5b 100644
--- a/src/Marr.Data/Reflection/SimpleReflectionStrategy.cs
+++ b/src/Marr.Data/Reflection/SimpleReflectionStrategy.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
diff --git a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs
index 385a9b989..5a767365d 100644
--- a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs
+++ b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs
@@ -21,19 +21,32 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
public void schema_should_have_proper_fields()
{
var model = new TestModel
- {
- FirstName = "Bob",
- LastName = "Poop"
- };
+ {
+ FirstName = "Bob",
+ LastName = "Poop"
+ };
var schema = SchemaBuilder.ToSchema(model);
- schema.Should().Contain(c => c.Order == 1 && c.Name == "LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string) c.Value == "Poop");
- schema.Should().Contain(c => c.Order == 0 && c.Name == "FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string) c.Value == "Bob");
+ schema.Should().Contain(c => c.Order == 1 && c.Name == "LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop");
+ schema.Should().Contain(c => c.Order == 0 && c.Name == "FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob");
}
- }
+ [Test]
+ public void schema_should_have_nested_fields()
+ {
+ var model = new NestedTestModel();
+ model.Name.FirstName = "Bob";
+ model.Name.LastName = "Poop";
+
+ var schema = SchemaBuilder.ToSchema(model);
+
+ schema.Should().Contain(c => c.Order == 0 && c.Name == "Name.FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob");
+ schema.Should().Contain(c => c.Order == 1 && c.Name == "Name.LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop");
+ schema.Should().Contain(c => c.Order == 2 && c.Name == "Quote" && c.Label == "Quote" && c.HelpText == "Your Favorite Quote");
+ }
+ }
public class TestModel
{
@@ -45,4 +58,13 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
public string Other { get; set; }
}
-}
\ No newline at end of file
+
+ public class NestedTestModel
+ {
+ [FieldDefinition(0)]
+ public TestModel Name { get; set; } = new TestModel();
+
+ [FieldDefinition(1, Label = "Quote", HelpText = "Your Favorite Quote")]
+ public string Quote { get; set; }
+ }
+}
diff --git a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj
index 69402ccc5..9d787542e 100644
--- a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj
+++ b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj
@@ -48,8 +48,8 @@
..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll
-
- ..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll
+
+ True..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll
diff --git a/src/NzbDrone.Api.Test/packages.config b/src/NzbDrone.Api.Test/packages.config
index b329faeb1..0913b6416 100644
--- a/src/NzbDrone.Api.Test/packages.config
+++ b/src/NzbDrone.Api.Test/packages.config
@@ -1,7 +1,7 @@
-
+
\ No newline at end of file
diff --git a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs b/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs
index f6efc16ce..4feebbdb4 100644
--- a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs
+++ b/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs
@@ -33,12 +33,12 @@ namespace NzbDrone.Api.Authentication
{
if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms)
{
- RegisterFormsAuth(pipelines);
+ RegisterFormsAuth(pipelines);
}
else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic)
{
- pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr"));
+ pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr"));
}
pipelines.BeforeRequest.AddItemToEndOfPipeline((Func) RequiresAuthentication);
@@ -64,10 +64,13 @@ namespace NzbDrone.Api.Authentication
new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt)))
);
+ FormsAuthentication.FormsAuthenticationCookieName = "_ncfa_sonarr";
+
FormsAuthentication.Enable(pipelines, new FormsAuthenticationConfiguration
{
RedirectUrl = _configFileProvider.UrlBase + "/login",
UserMapper = _authenticationService,
+ Path = _configFileProvider.UrlBase,
CryptographyConfiguration = cryptographyConfiguration
});
}
diff --git a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs
index 4845bc653..aec7320e7 100644
--- a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs
+++ b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs
@@ -1,9 +1,10 @@
-using Nancy;
+using Nancy;
using System;
using System.Collections.Generic;
using System.Linq;
using Ical.Net;
using Ical.Net.DataTypes;
+using Ical.Net.General;
using Ical.Net.Interfaces.Serialization;
using Ical.Net.Serialization;
using Ical.Net.Serialization.iCalendar.Factory;
@@ -92,7 +93,9 @@ namespace NzbDrone.Api.Calendar
ProductId = "-//sonarr.tv//Sonarr//EN"
};
-
+ var calendarName = "Sonarr TV Schedule";
+ calendar.AddProperty(new CalendarProperty("NAME", calendarName));
+ calendar.AddProperty(new CalendarProperty("X-WR-CALNAME", calendarName));
foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value))
{
@@ -114,7 +117,7 @@ namespace NzbDrone.Api.Calendar
if (asAllDay)
{
- occurrence.Start = new CalDateTime(episode.AirDateUtc.Value) { HasTime = false };
+ occurrence.Start = new CalDateTime(episode.AirDateUtc.Value.ToLocalTime()) { HasTime = false };
}
else
{
diff --git a/src/NzbDrone.Api/ClientSchema/Field.cs b/src/NzbDrone.Api/ClientSchema/Field.cs
index ec611e8d6..ff9f6aebd 100644
--- a/src/NzbDrone.Api/ClientSchema/Field.cs
+++ b/src/NzbDrone.Api/ClientSchema/Field.cs
@@ -7,11 +7,17 @@ namespace NzbDrone.Api.ClientSchema
public int Order { get; set; }
public string Name { get; set; }
public string Label { get; set; }
+ public string Unit { get; set; }
public string HelpText { get; set; }
public string HelpLink { get; set; }
public object Value { get; set; }
public string Type { get; set; }
public bool Advanced { get; set; }
public List SelectOptions { get; set; }
+
+ public Field Clone()
+ {
+ return (Field)MemberwiseClone();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Api/ClientSchema/FieldMapping.cs b/src/NzbDrone.Api/ClientSchema/FieldMapping.cs
new file mode 100644
index 000000000..93e90b792
--- /dev/null
+++ b/src/NzbDrone.Api/ClientSchema/FieldMapping.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Api.ClientSchema
+{
+ public class FieldMapping
+ {
+ public Field Field { get; set; }
+ public Type PropertyType { get; set; }
+ public Func
- ..\packages\NLog.4.4.3\lib\net40\NLog.dll
+ ..\packages\NLog.4.5.3\lib\net40-client\NLog.dll..\packages\Ical.Net.2.2.32\lib\net40\NodaTime.dll
+
@@ -83,6 +84,10 @@
False..\Libraries\Sqlite\System.Data.SQLite.dll
+
+
+
+
@@ -99,13 +104,16 @@
+
+
+
diff --git a/src/NzbDrone.Api/Parse/ParseModule.cs b/src/NzbDrone.Api/Parse/ParseModule.cs
index df36307ff..266f66eb4 100644
--- a/src/NzbDrone.Api/Parse/ParseModule.cs
+++ b/src/NzbDrone.Api/Parse/ParseModule.cs
@@ -1,5 +1,6 @@
using NzbDrone.Api.Episodes;
using NzbDrone.Api.Series;
+using NzbDrone.Common.Extensions;
using NzbDrone.Core.Parser;
namespace NzbDrone.Api.Parse
@@ -18,7 +19,8 @@ namespace NzbDrone.Api.Parse
private ParseResource Parse()
{
var title = Request.Query.Title.Value as string;
- var parsedEpisodeInfo = Parser.ParseTitle(title);
+ var path = Request.Query.Path.Value as string;
+ var parsedEpisodeInfo = path.IsNotNullOrWhiteSpace() ? Parser.ParsePath(path) : Parser.ParseTitle(title);
if (parsedEpisodeInfo == null)
{
diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs
index b45727227..d7ad2ec67 100644
--- a/src/NzbDrone.Api/ProviderModuleBase.cs
+++ b/src/NzbDrone.Api/ProviderModuleBase.cs
@@ -210,7 +210,12 @@ namespace NzbDrone.Api
protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings)
{
- var result = new NzbDroneValidationResult(validationResult.Errors);
+ var result = validationResult as NzbDroneValidationResult;
+
+ if (result == null)
+ {
+ result = new NzbDroneValidationResult(validationResult.Errors);
+ }
if (includeWarnings && (!result.IsValid || result.HasWarnings))
{
diff --git a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs
index e87e581de..30303ab73 100644
--- a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs
+++ b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Validation.Paths;
@@ -17,7 +17,9 @@ namespace NzbDrone.Api.RootFolders
DroneFactoryValidator droneFactoryValidator,
MappedNetworkDriveValidator mappedNetworkDriveValidator,
StartupFolderValidator startupFolderValidator,
- FolderWritableValidator folderWritableValidator)
+ SystemFolderValidator systemFolderValidator,
+ FolderWritableValidator folderWritableValidator
+ )
: base(signalRBroadcaster)
{
_rootFolderService = rootFolderService;
@@ -35,6 +37,7 @@ namespace NzbDrone.Api.RootFolders
.SetValidator(mappedNetworkDriveValidator)
.SetValidator(startupFolderValidator)
.SetValidator(pathExistsValidator)
+ .SetValidator(systemFolderValidator)
.SetValidator(folderWritableValidator);
}
@@ -60,4 +63,4 @@ namespace NzbDrone.Api.RootFolders
_rootFolderService.Remove(id);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Api/RootFolders/RootFolderResource.cs b/src/NzbDrone.Api/RootFolders/RootFolderResource.cs
index 86efef529..df55fcf1a 100644
--- a/src/NzbDrone.Api/RootFolders/RootFolderResource.cs
+++ b/src/NzbDrone.Api/RootFolders/RootFolderResource.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using NzbDrone.Api.REST;
using NzbDrone.Core.RootFolders;
@@ -9,6 +9,7 @@ namespace NzbDrone.Api.RootFolders
{
public string Path { get; set; }
public long? FreeSpace { get; set; }
+ public long? TotalSpace { get; set; }
public List UnmappedFolders { get; set; }
}
@@ -25,6 +26,7 @@ namespace NzbDrone.Api.RootFolders
Path = model.Path,
FreeSpace = model.FreeSpace,
+ TotalSpace = model.TotalSpace,
UnmappedFolders = model.UnmappedFolders
};
}
@@ -48,4 +50,4 @@ namespace NzbDrone.Api.RootFolders
return models.Select(ToResource).ToList();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Api/Series/SeasonResource.cs b/src/NzbDrone.Api/Series/SeasonResource.cs
index 4c20d8865..6115b8ab8 100644
--- a/src/NzbDrone.Api/Series/SeasonResource.cs
+++ b/src/NzbDrone.Api/Series/SeasonResource.cs
@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
+using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Tv;
namespace NzbDrone.Api.Series
{
@@ -8,18 +9,20 @@ namespace NzbDrone.Api.Series
public int SeasonNumber { get; set; }
public bool Monitored { get; set; }
public SeasonStatisticsResource Statistics { get; set; }
+ public List Images { get; set; }
}
public static class SeasonResourceMapper
{
- public static SeasonResource ToResource(this Season model)
+ public static SeasonResource ToResource(this Season model, bool includeImages = false)
{
if (model == null) return null;
return new SeasonResource
{
SeasonNumber = model.SeasonNumber,
- Monitored = model.Monitored
+ Monitored = model.Monitored,
+ Images = includeImages ? model.Images : null
};
}
@@ -30,13 +33,14 @@ namespace NzbDrone.Api.Series
return new Season
{
SeasonNumber = resource.SeasonNumber,
- Monitored = resource.Monitored
+ Monitored = resource.Monitored,
+ Images = resource.Images
};
}
- public static List ToResource(this IEnumerable models)
+ public static List ToResource(this IEnumerable models, bool includeImages = false)
{
- return models.Select(ToResource).ToList();
+ return models.Select(s => ToResource(s, includeImages)).ToList();
}
public static List ToModel(this IEnumerable resources)
diff --git a/src/NzbDrone.Api/Series/SeriesEditorModule.cs b/src/NzbDrone.Api/Series/SeriesEditorModule.cs
index 87cd53113..d68fa7aa4 100644
--- a/src/NzbDrone.Api/Series/SeriesEditorModule.cs
+++ b/src/NzbDrone.Api/Series/SeriesEditorModule.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using Nancy;
using NzbDrone.Api.Extensions;
@@ -24,7 +24,7 @@ namespace NzbDrone.Api.Series
var series = resources.Select(seriesResource => seriesResource.ToModel(_seriesService.GetSeries(seriesResource.Id))).ToList();
return _seriesService.UpdateSeries(series)
- .ToResource()
+ .ToResource(false)
.AsResponse(HttpStatusCode.Accepted);
}
}
diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs
index 0b33b9ee3..2ad1012e0 100644
--- a/src/NzbDrone.Api/Series/SeriesModule.cs
+++ b/src/NzbDrone.Api/Series/SeriesModule.cs
@@ -1,7 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
+using NzbDrone.Api.Extensions;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaCover;
@@ -45,6 +46,7 @@ namespace NzbDrone.Api.Series
SeriesExistsValidator seriesExistsValidator,
DroneFactoryValidator droneFactoryValidator,
SeriesAncestorValidator seriesAncestorValidator,
+ SystemFolderValidator systemFolderValidator,
ProfileExistsValidator profileExistsValidator
)
: base(signalRBroadcaster)
@@ -71,6 +73,7 @@ namespace NzbDrone.Api.Series
.SetValidator(seriesPathValidator)
.SetValidator(droneFactoryValidator)
.SetValidator(seriesAncestorValidator)
+ .SetValidator(systemFolderValidator)
.When(s => !s.Path.IsNullOrWhiteSpace());
SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator);
@@ -84,26 +87,17 @@ namespace NzbDrone.Api.Series
private SeriesResource GetSeries(int id)
{
+ var includeSeasonImages = Context != null && Request.GetBooleanQueryParameter("includeSeasonImages");
+
var series = _seriesService.GetSeries(id);
- return MapToResource(series);
- }
-
- private SeriesResource MapToResource(Core.Tv.Series series)
- {
- if (series == null) return null;
-
- var resource = series.ToResource();
- MapCoversToLocal(resource);
- FetchAndLinkSeriesStatistics(resource);
- PopulateAlternateTitles(resource);
-
- return resource;
+ return MapToResource(series, includeSeasonImages);
}
private List AllSeries()
{
+ var includeSeasonImages = Request.GetBooleanQueryParameter("includeSeasonImages");
var seriesStats = _seriesStatisticsService.SeriesStatistics();
- var seriesResources = _seriesService.GetAllSeries().ToResource();
+ var seriesResources = _seriesService.GetAllSeries().Select(s => s.ToResource(includeSeasonImages)).ToList();
MapCoversToLocal(seriesResources.ToArray());
LinkSeriesStatistics(seriesResources, seriesStats);
@@ -141,6 +135,18 @@ namespace NzbDrone.Api.Series
_seriesService.DeleteSeries(id, deleteFiles);
}
+ private SeriesResource MapToResource(Core.Tv.Series series, bool includeSeasonImages)
+ {
+ if (series == null) return null;
+
+ var resource = series.ToResource(includeSeasonImages);
+ MapCoversToLocal(resource);
+ FetchAndLinkSeriesStatistics(resource);
+ PopulateAlternateTitles(resource);
+
+ return resource;
+ }
+
private void MapCoversToLocal(params SeriesResource[] series)
{
foreach (var seriesResource in series)
diff --git a/src/NzbDrone.Api/Series/SeriesResource.cs b/src/NzbDrone.Api/Series/SeriesResource.cs
index 86de03eb7..b973ad30d 100644
--- a/src/NzbDrone.Api/Series/SeriesResource.cs
+++ b/src/NzbDrone.Api/Series/SeriesResource.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Api.REST;
@@ -97,7 +97,7 @@ namespace NzbDrone.Api.Series
public static class SeriesResourceMapper
{
- public static SeriesResource ToResource(this Core.Tv.Series model)
+ public static SeriesResource ToResource(this Core.Tv.Series model, bool includeSeasonImages = false)
{
if (model == null) return null;
@@ -121,7 +121,7 @@ namespace NzbDrone.Api.Series
AirTime = model.AirTime,
Images = model.Images,
- Seasons = model.Seasons.ToResource(),
+ Seasons = model.Seasons.ToResource(includeSeasonImages),
Year = model.Year,
Path = model.Path,
@@ -214,9 +214,9 @@ namespace NzbDrone.Api.Series
return series;
}
- public static List ToResource(this IEnumerable series)
+ public static List ToResource(this IEnumerable series, bool includeSeasonImages)
{
- return series.Select(ToResource).ToList();
+ return series.Select(s => ToResource(s, includeSeasonImages)).ToList();
}
}
}
diff --git a/src/NzbDrone.Api/System/Backup/BackupModule.cs b/src/NzbDrone.Api/System/Backup/BackupModule.cs
index b5074793e..8874ad420 100644
--- a/src/NzbDrone.Api/System/Backup/BackupModule.cs
+++ b/src/NzbDrone.Api/System/Backup/BackupModule.cs
@@ -21,9 +21,9 @@ namespace NzbDrone.Api.System.Backup
return backups.Select(b => new BackupResource
{
- Id = b.Path.GetHashCode(),
- Name = Path.GetFileName(b.Path),
- Path = b.Path,
+ Id = b.Name.GetHashCode(),
+ Name = b.Name,
+ Path = $"/backup/{b.Type.ToString().ToLower()}/{b.Name}",
Type = b.Type,
Time = b.Time
}).ToList();
diff --git a/src/NzbDrone.Api/packages.config b/src/NzbDrone.Api/packages.config
index f68f69f6b..ba6f1b3ed 100644
--- a/src/NzbDrone.Api/packages.config
+++ b/src/NzbDrone.Api/packages.config
@@ -6,5 +6,5 @@
-
+
\ No newline at end of file
diff --git a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj
index 1d6e658f9..5c28f26e2 100644
--- a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj
+++ b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj
@@ -47,20 +47,27 @@
..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll
+
+
+ ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll
+ True
+
- ..\packages\NLog.4.4.3\lib\net40\NLog.dll
+ ..\packages\NLog.4.5.3\lib\net40-client\NLog.dll..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll
+
+
+
+
+
-
- ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll
-
diff --git a/src/NzbDrone.App.Test/packages.config b/src/NzbDrone.App.Test/packages.config
index dc7aef2ad..81a4765e8 100644
--- a/src/NzbDrone.App.Test/packages.config
+++ b/src/NzbDrone.App.Test/packages.config
@@ -1,8 +1,8 @@
-
+
-
+
\ No newline at end of file
diff --git a/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj b/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj
index d3861c667..72a909bbf 100644
--- a/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj
+++ b/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj
@@ -45,16 +45,20 @@
..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll
- ..\packages\NLog.4.4.3\lib\net40\NLog.dll
+ ..\packages\NLog.4.5.3\lib\net40-client\NLog.dll..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll
+
+
+
+
diff --git a/src/NzbDrone.Automation.Test/packages.config b/src/NzbDrone.Automation.Test/packages.config
index c5405c724..f92f63790 100644
--- a/src/NzbDrone.Automation.Test/packages.config
+++ b/src/NzbDrone.Automation.Test/packages.config
@@ -1,7 +1,7 @@
-
+
diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs
index d6c4faece..d90d8e53d 100644
--- a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs
+++ b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Linq;
using Moq;
@@ -238,7 +238,7 @@ namespace NzbDrone.Common.Test.DiskTests
WithExistingFile(_targetPath);
- Assert.Throws(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move, false));
+ Assert.Throws(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move, false));
Mocker.GetMock()
.Verify(v => v.DeleteFile(_targetPath), Times.Never());
diff --git a/src/NzbDrone.Common.Test/ExtensionTests/UrlExtensionsFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/UrlExtensionsFixture.cs
new file mode 100644
index 000000000..eae0736dc
--- /dev/null
+++ b/src/NzbDrone.Common.Test/ExtensionTests/UrlExtensionsFixture.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Common.Extensions;
+
+namespace NzbDrone.Common.Test.ExtensionTests
+{
+ [TestFixture]
+ public class UrlExtensionsFixture
+ {
+ [TestCase("http://my.local/url")]
+ [TestCase("https://my.local/url")]
+ public void should_report_as_valid_url(string url)
+ {
+ url.IsValidUrl().Should().BeTrue();
+ }
+
+ [TestCase("")]
+ [TestCase(" http://my.local/url")]
+ [TestCase("http://my.local/url ")]
+ public void should_report_as_invalid_url(string url)
+ {
+ url.IsValidUrl().Should().BeFalse();
+ }
+ }
+}
diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs
index 23d65c322..49cdcc6e5 100644
--- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs
+++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs
@@ -61,7 +61,7 @@ namespace NzbDrone.Common.Test.Http
response.Content.Should().NotBeNullOrWhiteSpace();
}
-
+
[Test]
public void should_execute_https_get()
{
@@ -139,7 +139,54 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = true;
- Subject.Get(request);
+ var response = Subject.Get(request);
+
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ ExceptionVerification.ExpectedErrors(0);
+ }
+
+ [Test]
+ public void should_not_follow_redirects()
+ {
+ var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
+ request.AllowAutoRedirect = false;
+
+ var response = Subject.Get(request);
+
+ response.StatusCode.Should().Be(HttpStatusCode.Found);
+
+ ExceptionVerification.ExpectedErrors(1);
+ }
+
+ [Test]
+ public void should_follow_redirects_to_https()
+ {
+ if (typeof(TDispatcher) == typeof(ManagedHttpDispatcher) && PlatformInfo.IsMono)
+ {
+ Assert.Ignore("Will fail on tls1.2 via managed dispatcher, ignore.");
+ }
+
+ var request = new HttpRequestBuilder($"http://{_httpBinHost}/redirect-to")
+ .AddQueryParam("url", $"https://sonarr.tv/")
+ .Build();
+ request.AllowAutoRedirect = true;
+
+ var response = Subject.Get(request);
+
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+ response.Content.Should().Contain("Sonarr");
+
+ ExceptionVerification.ExpectedErrors(0);
+ }
+
+ [Test]
+ public void should_throw_on_too_many_redirects()
+ {
+ var request = new HttpRequest($"http://{_httpBinHost}/redirect/4");
+ request.AllowAutoRedirect = true;
+
+ Assert.Throws(() => Subject.Get(request));
ExceptionVerification.ExpectedErrors(0);
}
@@ -238,19 +285,96 @@ namespace NzbDrone.Common.Test.Http
response.Resource.Headers.Should().NotContainKey("Cookie");
}
+ [Test]
+ public void should_not_store_request_cookie()
+ {
+ var requestGet = new HttpRequest($"http://{_httpBinHost}/get");
+ requestGet.Cookies.Add("my", "cookie");
+ requestGet.AllowAutoRedirect = false;
+ requestGet.StoreRequestCookie = false;
+ requestGet.StoreResponseCookie = false;
+ var responseGet = Subject.Get(requestGet);
+
+ var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestCookies.AllowAutoRedirect = false;
+ var responseCookies = Subject.Get(requestCookies);
+
+ responseCookies.Resource.Cookies.Should().BeEmpty();
+
+ ExceptionVerification.IgnoreErrors();
+ }
+
+ [Test]
+ public void should_store_request_cookie()
+ {
+ var requestGet = new HttpRequest($"http://{_httpBinHost}/get");
+ requestGet.Cookies.Add("my", "cookie");
+ requestGet.AllowAutoRedirect = false;
+ requestGet.StoreRequestCookie.Should().BeTrue();
+ requestGet.StoreResponseCookie = false;
+ var responseGet = Subject.Get(requestGet);
+
+ var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestCookies.AllowAutoRedirect = false;
+ var responseCookies = Subject.Get(requestCookies);
+
+ responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
+
+ ExceptionVerification.IgnoreErrors();
+ }
+
+ [Test]
+ public void should_delete_request_cookie()
+ {
+ var requestDelete = new HttpRequest($"http://{_httpBinHost}/cookies/delete?my");
+ requestDelete.Cookies.Add("my", "cookie");
+ requestDelete.AllowAutoRedirect = true;
+ requestDelete.StoreRequestCookie = false;
+ requestDelete.StoreResponseCookie = false;
+
+ // Delete and redirect since that's the only way to check the internal temporary cookie container
+ var responseCookies = Subject.Get(requestDelete);
+
+ responseCookies.Resource.Cookies.Should().BeEmpty();
+ }
+
+ [Test]
+ public void should_clear_request_cookie()
+ {
+ var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestSet.Cookies.Add("my", "cookie");
+ requestSet.AllowAutoRedirect = false;
+ requestSet.StoreRequestCookie = true;
+ requestSet.StoreResponseCookie = false;
+
+ var responseSet = Subject.Get(requestSet);
+
+ var requestClear = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestClear.Cookies.Add("my", null);
+ requestClear.AllowAutoRedirect = false;
+ requestClear.StoreRequestCookie = true;
+ requestClear.StoreResponseCookie = false;
+
+ var responseClear = Subject.Get(requestClear);
+
+ responseClear.Resource.Cookies.Should().BeEmpty();
+ }
+
[Test]
public void should_not_store_response_cookie()
{
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
+ requestSet.StoreRequestCookie = false;
+ requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get(requestSet);
- var request = new HttpRequest($"http://{_httpBinHost}/get");
+ var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
- var response = Subject.Get(request);
+ var responseCookies = Subject.Get(requestCookies);
- response.Resource.Headers.Should().NotContainKey("Cookie");
+ responseCookies.Resource.Cookies.Should().BeEmpty();
ExceptionVerification.IgnoreErrors();
}
@@ -260,19 +384,31 @@ namespace NzbDrone.Common.Test.Http
{
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
+ requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
- var request = new HttpRequest($"http://{_httpBinHost}/get");
+ var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
- var response = Subject.Get(request);
+ var responseCookies = Subject.Get(requestCookies);
- response.Resource.Headers.Should().ContainKey("Cookie");
+ responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
- var cookie = response.Resource.Headers["Cookie"].ToString();
+ ExceptionVerification.IgnoreErrors();
+ }
- cookie.Should().Contain("my=cookie");
+ [Test]
+ public void should_temp_store_response_cookie()
+ {
+ var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
+ requestSet.AllowAutoRedirect = true;
+ requestSet.StoreRequestCookie = false;
+ requestSet.StoreResponseCookie.Should().BeFalse();
+ var responseSet = Subject.Get(requestSet);
+
+ // Set and redirect since that's the only way to check the internal temporary cookie container
+ responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
ExceptionVerification.IgnoreErrors();
}
@@ -281,21 +417,129 @@ namespace NzbDrone.Common.Test.Http
public void should_overwrite_response_cookie()
{
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
+ requestSet.Cookies.Add("my", "oldcookie");
requestSet.AllowAutoRedirect = false;
+ requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true;
- requestSet.Cookies["my"] = "oldcookie";
var responseSet = Subject.Get(requestSet);
- var request = new HttpRequest($"http://{_httpBinHost}/get");
+ var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
- var response = Subject.Get(request);
+ var responseCookies = Subject.Get(requestCookies);
- response.Resource.Headers.Should().ContainKey("Cookie");
+ responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
- var cookie = response.Resource.Headers["Cookie"].ToString();
+ ExceptionVerification.IgnoreErrors();
+ }
- cookie.Should().Contain("my=cookie");
+ [Test]
+ public void should_overwrite_temp_response_cookie()
+ {
+ var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
+ requestSet.Cookies.Add("my", "oldcookie");
+ requestSet.AllowAutoRedirect = true;
+ requestSet.StoreRequestCookie = true;
+ requestSet.StoreResponseCookie = false;
+
+ var responseSet = Subject.Get(requestSet);
+
+ responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
+
+ var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
+
+ var responseCookies = Subject.Get(requestCookies);
+
+ responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "oldcookie");
+
+ ExceptionVerification.IgnoreErrors();
+ }
+
+ [Test]
+ public void should_not_delete_response_cookie()
+ {
+ var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestCookies.Cookies.Add("my", "cookie");
+ requestCookies.AllowAutoRedirect = false;
+ requestCookies.StoreRequestCookie = true;
+ requestCookies.StoreResponseCookie = false;
+ var responseCookies = Subject.Get(requestCookies);
+
+ responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
+
+ var requestDelete = new HttpRequest($"http://{_httpBinHost}/cookies/delete?my");
+ requestDelete.AllowAutoRedirect = false;
+ requestDelete.StoreRequestCookie = false;
+ requestDelete.StoreResponseCookie = false;
+
+ var responseDelete = Subject.Get(requestDelete);
+
+ requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestCookies.StoreRequestCookie = false;
+ requestCookies.StoreResponseCookie = false;
+
+ responseCookies = Subject.Get(requestCookies);
+
+ responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
+
+ ExceptionVerification.IgnoreErrors();
+ }
+
+ [Test]
+ public void should_delete_response_cookie()
+ {
+ var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestCookies.Cookies.Add("my", "cookie");
+ requestCookies.AllowAutoRedirect = false;
+ requestCookies.StoreRequestCookie = true;
+ requestCookies.StoreResponseCookie = false;
+ var responseCookies = Subject.Get(requestCookies);
+
+ responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
+
+ var requestDelete = new HttpRequest($"http://{_httpBinHost}/cookies/delete?my");
+ requestDelete.AllowAutoRedirect = false;
+ requestDelete.StoreRequestCookie = false;
+ requestDelete.StoreResponseCookie = true;
+
+ var responseDelete = Subject.Get(requestDelete);
+
+ requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestCookies.StoreRequestCookie = false;
+ requestCookies.StoreResponseCookie = false;
+
+ responseCookies = Subject.Get(requestCookies);
+
+ responseCookies.Resource.Cookies.Should().BeEmpty();
+
+ ExceptionVerification.IgnoreErrors();
+ }
+
+ [Test]
+ public void should_delete_temp_response_cookie()
+ {
+ var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestCookies.Cookies.Add("my", "cookie");
+ requestCookies.AllowAutoRedirect = false;
+ requestCookies.StoreRequestCookie = true;
+ requestCookies.StoreResponseCookie = false;
+ var responseCookies = Subject.Get(requestCookies);
+
+ responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
+
+ var requestDelete = new HttpRequest($"http://{_httpBinHost}/cookies/delete?my");
+ requestDelete.AllowAutoRedirect = true;
+ requestDelete.StoreRequestCookie = false;
+ requestDelete.StoreResponseCookie = false;
+ var responseDelete = Subject.Get(requestDelete);
+
+ responseDelete.Resource.Cookies.Should().BeEmpty();
+
+ requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
+ requestCookies.StoreRequestCookie = false;
+ requestCookies.StoreResponseCookie = false;
+
+ responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
ExceptionVerification.IgnoreErrors();
}
@@ -407,4 +651,9 @@ namespace NzbDrone.Common.Test.Http
public string Url { get; set; }
public string Data { get; set; }
}
-}
\ No newline at end of file
+
+ public class HttpCookieResource
+ {
+ public Dictionary Cookies { get; set; }
+ }
+}
diff --git a/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs b/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs
index 099ab990f..dea6f64cd 100644
--- a/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs
+++ b/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs
@@ -7,6 +7,13 @@ namespace NzbDrone.Common.Test.Http
{
public class HttpUriFixture : TestBase
{
+ [TestCase("abc://my_host.com:8080/root/api/")]
+ public void should_parse(string uri)
+ {
+ var newUri = new HttpUri(uri);
+ newUri.FullUri.Should().Be(uri);
+ }
+
[TestCase("", "", "")]
[TestCase("/", "", "/")]
[TestCase("base", "", "base")]
@@ -77,7 +84,7 @@ namespace NzbDrone.Common.Test.Http
public void should_combine_relative_path(string basePath, string relativePath, string expected)
{
var newUri = new HttpUri(basePath).CombinePath(relativePath);
-
+
newUri.FullUri.Should().Be(expected);
}
}
diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs
index 00c22cffb..b2ebdef47 100644
--- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs
+++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs
@@ -15,6 +15,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"https://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user=sonarr&api=mySecret&eng=1")]
[TestCase(@"https://dognzb.cr/fetch/2b51db35e1912ffc138825a12b9933d2/2b51db35e1910123321025a12b9933d2")]
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
+ [TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
[TestCase(@"{ ""Name"" : ""Server1.Username"", ""Value"" : ""mySecret"" }, { ""Name"" : ""Server1.Password"", ""Value"" : ""mySecret"" }, ")]
diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj
index 8f80dbe36..c6ed9935e 100644
--- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj
+++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj
@@ -43,23 +43,28 @@
..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll
+
+ ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll
+ True
+
- ..\packages\NLog.4.4.3\lib\net40\NLog.dll
+ ..\packages\NLog.4.5.3\lib\net40-client\NLog.dll..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll
+
+
+
+
-
- ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll
-
@@ -80,6 +85,7 @@
+
diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs
index e3e7fb34a..ec5451029 100644
--- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs
+++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs
@@ -86,7 +86,7 @@ namespace NzbDrone.Common.Test
{
first.AsOsAgnostic().PathEquals(second.AsOsAgnostic()).Should().BeFalse();
}
-
+
[Test]
public void should_return_false_when_not_a_child()
{
@@ -113,6 +113,7 @@ namespace NzbDrone.Common.Test
[TestCase(@"C:\Test\", @"C:\Test\mydir")]
[TestCase(@"C:\Test\", @"C:\Test\mydir\")]
[TestCase(@"C:\Test", @"C:\Test\30.Rock.S01E01.Pilot.avi")]
+ [TestCase(@"C:\", @"C:\Test\30.Rock.S01E01.Pilot.avi")]
public void path_should_be_parent(string parentPath, string childPath)
{
parentPath.AsOsAgnostic().IsParentPath(childPath.AsOsAgnostic()).Should().BeTrue();
diff --git a/src/NzbDrone.Common.Test/packages.config b/src/NzbDrone.Common.Test/packages.config
index a974434ae..69062660f 100644
--- a/src/NzbDrone.Common.Test/packages.config
+++ b/src/NzbDrone.Common.Test/packages.config
@@ -1,7 +1,7 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/NzbDrone.Common/ConsoleService.cs b/src/NzbDrone.Common/ConsoleService.cs
index 321831277..262420f24 100644
--- a/src/NzbDrone.Common/ConsoleService.cs
+++ b/src/NzbDrone.Common/ConsoleService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Diagnostics;
using System.IO;
using NzbDrone.Common.EnvironmentInfo;
@@ -24,6 +24,8 @@ namespace NzbDrone.Common
Console.WriteLine(" /{0} Install the application as a Windows Service ({1}).", StartupContext.INSTALL_SERVICE, ServiceProvider.NZBDRONE_SERVICE_NAME);
Console.WriteLine(" /{0} Uninstall already installed Windows Service ({1}).", StartupContext.UNINSTALL_SERVICE, ServiceProvider.NZBDRONE_SERVICE_NAME);
Console.WriteLine(" /{0} Don't open Sonarr in a browser", StartupContext.NO_BROWSER);
+ Console.WriteLine(" /{0} Start Sonarr terminating any other instances", StartupContext.TERMINATE);
+ Console.WriteLine(" /{0}=path Path to use as the AppData location (stores database, config, logs, etc)", StartupContext.APPDATA);
Console.WriteLine(" Run application in console mode.");
}
@@ -37,4 +39,4 @@ namespace NzbDrone.Common
Console.WriteLine("Can't find service ({0})", ServiceProvider.NZBDRONE_SERVICE_NAME);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs b/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs
new file mode 100644
index 000000000..986413742
--- /dev/null
+++ b/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs
@@ -0,0 +1,29 @@
+using System;
+using System.IO;
+using System.Runtime.Serialization;
+
+namespace NzbDrone.Common.Disk
+{
+ public class DestinationAlreadyExistsException : IOException
+ {
+ public DestinationAlreadyExistsException()
+ {
+ }
+
+ public DestinationAlreadyExistsException(string message) : base(message)
+ {
+ }
+
+ public DestinationAlreadyExistsException(string message, int hresult) : base(message, hresult)
+ {
+ }
+
+ public DestinationAlreadyExistsException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+
+ protected DestinationAlreadyExistsException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+ }
+}
diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs
index 6763709a5..ca3ead7cd 100644
--- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs
+++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs
@@ -200,6 +200,11 @@ namespace NzbDrone.Common.Disk
throw new IOException(string.Format("Source and destination can't be the same {0}", source));
}
+ CopyFileInternal(source, destination, overwrite);
+ }
+
+ protected virtual void CopyFileInternal(string source, string destination, bool overwrite = false)
+ {
File.Copy(source, destination, overwrite);
}
@@ -219,6 +224,11 @@ namespace NzbDrone.Common.Disk
}
RemoveReadOnly(source);
+ MoveFileInternal(source, destination);
+ }
+
+ protected virtual void MoveFileInternal(string source, string destination)
+ {
File.Move(source, destination);
}
diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs
index 3f93c11e4..3dedc38d0 100644
--- a/src/NzbDrone.Common/Disk/DiskTransferService.cs
+++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs
@@ -1,10 +1,11 @@
-using System;
+using System;
using System.IO;
using System.Linq;
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
@@ -223,7 +224,7 @@ namespace NzbDrone.Common.Disk
_diskProvider.MoveFile(sourcePath, tempPath, true);
try
{
- ClearTargetPath(targetPath, overwrite);
+ ClearTargetPath(sourcePath, targetPath, overwrite);
_diskProvider.MoveFile(tempPath, targetPath);
@@ -253,7 +254,7 @@ namespace NzbDrone.Common.Disk
throw new IOException(string.Format("Destination cannot be a child of the source [{0}] => [{1}]", sourcePath, targetPath));
}
- ClearTargetPath(targetPath, overwrite);
+ ClearTargetPath(sourcePath, targetPath, overwrite);
if (mode.HasFlag(TransferMode.HardLink))
{
@@ -330,7 +331,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))
{
@@ -340,7 +341,7 @@ namespace NzbDrone.Common.Disk
}
else
{
- throw new IOException(string.Format("Destination already exists [{0}]", targetPath));
+ throw new DestinationAlreadyExistsException($"Destination {targetPath} already exists.");
}
}
}
@@ -590,7 +591,7 @@ namespace NzbDrone.Common.Disk
private bool ShouldIgnore(FileInfo file)
{
- if (file.Name.StartsWith(".nfs"))
+ if (file.Name.StartsWith(".nfs") || file.Name == "debug.log" || file.Name.EndsWith(".socket"))
{
_logger.Trace("Ignoring file {0}", file.FullName);
return true;
diff --git a/src/NzbDrone.Common/Disk/DriveInfoMount.cs b/src/NzbDrone.Common/Disk/DriveInfoMount.cs
index ac039d719..40f4fb6cd 100644
--- a/src/NzbDrone.Common/Disk/DriveInfoMount.cs
+++ b/src/NzbDrone.Common/Disk/DriveInfoMount.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System.Collections.Generic;
+using System.IO;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Disk
@@ -8,10 +9,11 @@ namespace NzbDrone.Common.Disk
private readonly DriveInfo _driveInfo;
private readonly DriveType _driveType;
- public DriveInfoMount(DriveInfo driveInfo, DriveType driveType = DriveType.Unknown)
+ public DriveInfoMount(DriveInfo driveInfo, DriveType driveType = DriveType.Unknown, MountOptions mountOptions = null)
{
_driveInfo = driveInfo;
_driveType = driveType;
+ MountOptions = mountOptions;
}
public long AvailableFreeSpace => _driveInfo.AvailableFreeSpace;
@@ -33,6 +35,8 @@ namespace NzbDrone.Common.Disk
public bool IsReady => _driveInfo.IsReady;
+ public MountOptions MountOptions { get; private set; }
+
public string Name => _driveInfo.Name;
public string RootDirectory => _driveInfo.RootDirectory.FullName;
@@ -47,7 +51,7 @@ namespace NzbDrone.Common.Disk
{
get
{
- if (VolumeLabel.IsNullOrWhiteSpace())
+ if (VolumeLabel.IsNullOrWhiteSpace() || VolumeLabel.StartsWith("UUID=") || Name == VolumeLabel)
{
return Name;
}
diff --git a/src/NzbDrone.Common/Disk/IMount.cs b/src/NzbDrone.Common/Disk/IMount.cs
index 285673d69..3b15a4cb2 100644
--- a/src/NzbDrone.Common/Disk/IMount.cs
+++ b/src/NzbDrone.Common/Disk/IMount.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.IO;
namespace NzbDrone.Common.Disk
@@ -8,6 +9,7 @@ namespace NzbDrone.Common.Disk
string DriveFormat { get; }
DriveType DriveType { get; }
bool IsReady { get; }
+ MountOptions MountOptions { get; }
string Name { get; }
string RootDirectory { get; }
long TotalFreeSpace { get; }
diff --git a/src/NzbDrone.Common/Disk/MountOptions.cs b/src/NzbDrone.Common/Disk/MountOptions.cs
new file mode 100644
index 000000000..749c0a739
--- /dev/null
+++ b/src/NzbDrone.Common/Disk/MountOptions.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace NzbDrone.Common.Disk
+{
+ public class MountOptions
+ {
+ private readonly Dictionary _options;
+
+ public MountOptions(Dictionary options)
+ {
+ _options = options;
+ }
+
+ public bool IsReadOnly => _options.ContainsKey("ro");
+ }
+}
diff --git a/src/NzbDrone.Common/Exceptions/NotParentException.cs b/src/NzbDrone.Common/Disk/NotParentException.cs
similarity index 79%
rename from src/NzbDrone.Common/Exceptions/NotParentException.cs
rename to src/NzbDrone.Common/Disk/NotParentException.cs
index d9b78247e..0ae384722 100644
--- a/src/NzbDrone.Common/Exceptions/NotParentException.cs
+++ b/src/NzbDrone.Common/Disk/NotParentException.cs
@@ -1,4 +1,6 @@
-namespace NzbDrone.Common.Exceptions
+using NzbDrone.Common.Exceptions;
+
+namespace NzbDrone.Common.Disk
{
public class NotParentException : NzbDroneException
{
diff --git a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs
index 7132d539f..1018cbe34 100644
--- a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs
+++ b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs
@@ -1,8 +1,9 @@
-using System;
+using System;
using System.Security.AccessControl;
using System.Security.Principal;
using NLog;
using NzbDrone.Common.Disk;
+using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.EnvironmentInfo
@@ -27,12 +28,25 @@ namespace NzbDrone.Common.EnvironmentInfo
public void Register()
{
- _diskProvider.EnsureFolder(_appFolderInfo.AppDataFolder);
+ try
+ {
+ _diskProvider.EnsureFolder(_appFolderInfo.AppDataFolder);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ throw new SonarrStartupException("Cannot create AppFolder, Access to the path {0} is denied", _appFolderInfo.AppDataFolder);
+ }
+
if (OsInfo.IsWindows)
{
SetPermissions();
}
+
+ if (!_diskProvider.FolderWritable(_appFolderInfo.AppDataFolder))
+ {
+ throw new SonarrStartupException("AppFolder {0} is not writable", _appFolderInfo.AppDataFolder);
+ }
}
private void SetPermissions()
diff --git a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs
index cb432addc..d387001ef 100644
--- a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs
+++ b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs
@@ -1,5 +1,3 @@
-using System;
-
namespace NzbDrone.Common.EnvironmentInfo
{
public interface IRuntimeInfo
@@ -7,8 +5,9 @@ namespace NzbDrone.Common.EnvironmentInfo
bool IsUserInteractive { get; }
bool IsAdmin { get; }
bool IsWindowsService { get; }
+ bool IsWindowsTray { get; }
bool IsExiting { get; set; }
bool RestartPending { get; set; }
string ExecutingApplication { get; }
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs
index a53862311..3337b99b8 100644
--- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs
+++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs
@@ -1,10 +1,11 @@
-using System;
+using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.Principal;
using System.ServiceProcess;
using NLog;
+using NzbDrone.Common.Processes;
namespace NzbDrone.Common.EnvironmentInfo
{
@@ -27,6 +28,7 @@ namespace NzbDrone.Common.EnvironmentInfo
if (entry != null)
{
ExecutingApplication = entry.Location;
+ IsWindowsTray = entry.ManifestModule.Name == $"{ProcessProvider.NZB_DRONE_PROCESS_NAME}.exe";
}
}
@@ -102,5 +104,7 @@ namespace NzbDrone.Common.EnvironmentInfo
return true;
}
+
+ public bool IsWindowsTray { get; private set; }
}
}
diff --git a/src/NzbDrone.Common/Exceptions/SonarrStartupException.cs b/src/NzbDrone.Common/Exceptions/SonarrStartupException.cs
new file mode 100644
index 000000000..b61207642
--- /dev/null
+++ b/src/NzbDrone.Common/Exceptions/SonarrStartupException.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Common.Exceptions
+{
+ public class SonarrStartupException : NzbDroneException
+ {
+ public SonarrStartupException(string message, params object[] args)
+ : base("Sonarr failed to start: " + string.Format(message, args))
+ {
+
+ }
+
+ public SonarrStartupException(string message)
+ : base("Sonarr failed to start: " + message)
+ {
+
+ }
+
+ public SonarrStartupException()
+ : base("Sonarr failed to start")
+ {
+
+ }
+
+ public SonarrStartupException(Exception innerException, string message, params object[] args)
+ : base("Sonarr failed to start: " + string.Format(message, args), innerException)
+ {
+ }
+
+ public SonarrStartupException(Exception innerException, string message)
+ : base("Sonarr failed to start: " + message, innerException)
+ {
+ }
+
+ public SonarrStartupException(Exception innerException)
+ : base("Sonarr failed to start: " + innerException.Message)
+ {
+
+ }
+ }
+}
diff --git a/src/NzbDrone.Common/Extensions/ExceptionExtensions.cs b/src/NzbDrone.Common/Extensions/ExceptionExtensions.cs
new file mode 100644
index 000000000..ec07e73ed
--- /dev/null
+++ b/src/NzbDrone.Common/Extensions/ExceptionExtensions.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Common.Extensions
+{
+ public static class ExceptionExtensions
+ {
+ public static T WithData(this T ex, string key, string value) where T : Exception
+ {
+ ex.AddData(key, value);
+
+ return ex;
+ }
+ public static T WithData(this T ex, string key, int value) where T : Exception
+ {
+ ex.AddData(key, value.ToString());
+
+ return ex;
+ }
+
+ public static T WithData(this T ex, string key, Http.HttpUri value) where T : Exception
+ {
+ ex.AddData(key, value.ToString());
+
+ return ex;
+ }
+
+
+ public static T WithData(this T ex, Http.HttpResponse response, int maxSampleLength = 512) where T : Exception
+ {
+ if (response == null || response.Content == null) return ex;
+
+ var contentSample = response.Content.Substring(0, Math.Min(response.Content.Length, maxSampleLength));
+
+ if (response.Request != null)
+ {
+ ex.AddData("RequestUri", response.Request.Url.ToString());
+
+ if (response.Request.ContentSummary != null)
+ {
+ ex.AddData("RequestSummary", response.Request.ContentSummary);
+ }
+ }
+
+ ex.AddData("StatusCode", response.StatusCode.ToString());
+
+ if (response.Headers != null)
+ {
+ ex.AddData("ContentType", response.Headers.ContentType ?? string.Empty);
+ }
+ ex.AddData("ContentLength", response.Content.Length.ToString());
+ ex.AddData("ContentSample", contentSample);
+
+ return ex;
+ }
+
+
+ private static void AddData(this Exception ex, string key, string value)
+ {
+ if (value.IsNullOrWhiteSpace()) return;
+
+ ex.Data[key] = value;
+ }
+ }
+}
diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs
index a1beecaa9..2eeb2fe4e 100644
--- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs
+++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs
@@ -51,6 +51,34 @@ namespace NzbDrone.Common.Extensions
}
}
+ public static Dictionary ToDictionaryIgnoreDuplicates(this IEnumerable src, Func keySelector)
+ {
+ var result = new Dictionary();
+ foreach (var item in src)
+ {
+ var key = keySelector(item);
+ if (!result.ContainsKey(key))
+ {
+ result[key] = item;
+ }
+ }
+ return result;
+ }
+
+ public static Dictionary ToDictionaryIgnoreDuplicates(this IEnumerable src, Func keySelector, Func valueSelector)
+ {
+ var result = new Dictionary();
+ foreach (var item in src)
+ {
+ var key = keySelector(item);
+ if (!result.ContainsKey(key))
+ {
+ result[key] = valueSelector(item);
+ }
+ }
+ return result;
+ }
+
public static void AddIfNotNull(this List source, TSource item)
{
if (item == null)
@@ -81,4 +109,4 @@ namespace NzbDrone.Common.Extensions
return source.Select(predicate).ToList();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs
index e03f0a594..14da5fd51 100644
--- a/src/NzbDrone.Common/Extensions/PathExtensions.cs
+++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
@@ -59,7 +59,7 @@ namespace NzbDrone.Common.Extensions
{
if (!parentPath.IsParentPath(childPath))
{
- throw new Exceptions.NotParentException("{0} is not a child of {1}", childPath, parentPath);
+ throw new NotParentException("{0} is not a child of {1}", childPath, parentPath);
}
return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar);
@@ -80,11 +80,11 @@ namespace NzbDrone.Common.Extensions
public static bool IsParentPath(this string parentPath, string childPath)
{
- if (parentPath != "/")
+ if (parentPath != "/" && !parentPath.EndsWith(":\\"))
{
parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar);
}
- if (childPath != "/")
+ if (childPath != "/" && !parentPath.EndsWith(":\\"))
{
childPath = childPath.TrimEnd(Path.DirectorySeparatorChar);
}
@@ -276,4 +276,4 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs
index 247274e29..324a76eeb 100644
--- a/src/NzbDrone.Common/Extensions/StringExtensions.cs
+++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
@@ -63,6 +64,11 @@ namespace NzbDrone.Common.Extensions
return text;
}
+ public static string Join(this IEnumerable values, string separator)
+ {
+ return string.Join(separator, values);
+ }
+
public static string CleanSpaces(this string text)
{
return CollapseSpace.Replace(text, " ").Trim();
@@ -78,6 +84,16 @@ namespace NzbDrone.Common.Extensions
return !string.IsNullOrWhiteSpace(text);
}
+ public static bool StartsWithIgnoreCase(this string text, string startsWith)
+ {
+ return text.StartsWith(startsWith, StringComparison.InvariantCultureIgnoreCase);
+ }
+
+ public static bool EqualsIgnoreCase(this string text, string equals)
+ {
+ return text.Equals(equals, StringComparison.InvariantCultureIgnoreCase);
+ }
+
public static bool ContainsIgnoreCase(this string text, string contains)
{
return text.IndexOf(contains, StringComparison.InvariantCultureIgnoreCase) > -1;
@@ -118,4 +134,4 @@ namespace NzbDrone.Common.Extensions
return Encoding.ASCII.GetString(new [] { byteResult });
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Extensions/TryParseExtensions.cs b/src/NzbDrone.Common/Extensions/TryParseExtensions.cs
index c485fbd54..fe504b97b 100644
--- a/src/NzbDrone.Common/Extensions/TryParseExtensions.cs
+++ b/src/NzbDrone.Common/Extensions/TryParseExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
namespace NzbDrone.Common.Extensions
{
@@ -6,7 +7,7 @@ namespace NzbDrone.Common.Extensions
{
public static int? ParseInt32(this string source)
{
- int result = 0;
+ int result;
if (int.TryParse(source, out result))
{
@@ -16,9 +17,9 @@ namespace NzbDrone.Common.Extensions
return null;
}
- public static Nullable ParseInt64(this string source)
+ public static long? ParseInt64(this string source)
{
- long result = 0;
+ long result;
if (long.TryParse(source, out result))
{
@@ -27,5 +28,17 @@ namespace NzbDrone.Common.Extensions
return null;
}
+
+ public static double? ParseDouble(this string source)
+ {
+ double result;
+
+ if (double.TryParse(source.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+
+ return null;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Extensions/UrlExtensions.cs b/src/NzbDrone.Common/Extensions/UrlExtensions.cs
index b2dac6c19..50e0b9856 100644
--- a/src/NzbDrone.Common/Extensions/UrlExtensions.cs
+++ b/src/NzbDrone.Common/Extensions/UrlExtensions.cs
@@ -11,6 +11,11 @@ namespace NzbDrone.Common.Extensions
return false;
}
+ if (path.StartsWith(" ") || path.EndsWith(" "))
+ {
+ return false;
+ }
+
Uri uri;
if (!Uri.TryCreate(path, UriKind.Absolute, out uri))
{
diff --git a/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs
index 83d6fb1d1..17574982d 100644
--- a/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs
+++ b/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs
@@ -38,7 +38,7 @@ namespace NzbDrone.Common.Http.Dispatchers
_caBundleFilePath = _caBundleFileName;
}
}
-
+
public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, IUserAgentBuilder userAgentBuilder, Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
@@ -107,7 +107,7 @@ namespace NzbDrone.Common.Http.Dispatchers
throw new NotSupportedException($"HttpCurl method {request.Method} not supported");
}
curlEasy.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
- curlEasy.FollowLocation = request.AllowAutoRedirect;
+ curlEasy.FollowLocation = false;
if (request.RequestTimeout != TimeSpan.Zero)
{
diff --git a/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs
index 707004c9d..01b60e012 100644
--- a/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs
+++ b/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs
@@ -34,18 +34,11 @@ namespace NzbDrone.Common.Http.Dispatchers
{
return _managedDispatcher.GetResponse(request, cookies);
}
- catch (Exception ex)
+ catch (TlsFailureException)
{
- if (ex.ToString().Contains("The authentication or decryption has failed."))
- {
- _logger.Debug("https request failed in tls error for {0}, trying curl fallback.", request.Url.Host);
+ _logger.Debug("https request failed in tls error for {0}, trying curl fallback.", request.Url.Host);
- _curlTLSFallbackCache.Set(request.Url.Host, true);
- }
- else
- {
- throw;
- }
+ _curlTLSFallbackCache.Set(request.Url.Host, true);
}
}
diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs
index 60231f75e..4a7269468 100644
--- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs
+++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs
@@ -32,7 +32,7 @@ namespace NzbDrone.Common.Http.Dispatchers
webRequest.Method = request.Method.ToString();
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
webRequest.KeepAlive = request.ConnectionKeepAlive;
- webRequest.AllowAutoRedirect = request.AllowAutoRedirect;
+ webRequest.AllowAutoRedirect = false;
webRequest.CookieContainer = cookies;
if (request.RequestTimeout != TimeSpan.Zero)
@@ -47,19 +47,19 @@ namespace NzbDrone.Common.Http.Dispatchers
AddRequestHeaders(webRequest, request.Headers);
}
- if (request.ContentData != null)
- {
- webRequest.ContentLength = request.ContentData.Length;
- using (var writeStream = webRequest.GetRequestStream())
- {
- writeStream.Write(request.ContentData, 0, request.ContentData.Length);
- }
- }
-
HttpWebResponse httpWebResponse;
try
{
+ if (request.ContentData != null)
+ {
+ webRequest.ContentLength = request.ContentData.Length;
+ using (var writeStream = webRequest.GetRequestStream())
+ {
+ writeStream.Write(request.ContentData, 0, request.ContentData.Length);
+ }
+ }
+
httpWebResponse = (HttpWebResponse)webRequest.GetResponse();
}
catch (WebException e)
@@ -73,7 +73,27 @@ namespace NzbDrone.Common.Http.Dispatchers
if (httpWebResponse == null)
{
- throw;
+ // The default messages for WebException on mono are pretty horrible.
+ if (e.Status == WebExceptionStatus.NameResolutionFailure)
+ {
+ throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status);
+ }
+ else if (e.ToString().Contains("TLS Support not"))
+ {
+ throw new TlsFailureException(webRequest, e);
+ }
+ else if (e.ToString().Contains("The authentication or decryption has failed."))
+ {
+ throw new TlsFailureException(webRequest, e);
+ }
+ else if (OsInfo.IsNotWindows)
+ {
+ throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response);
+ }
+ else
+ {
+ throw;
+ }
}
}
@@ -83,7 +103,14 @@ namespace NzbDrone.Common.Http.Dispatchers
{
if (responseStream != null)
{
- data = responseStream.ToBytes();
+ try
+ {
+ data = responseStream.ToBytes();
+ }
+ catch (Exception ex)
+ {
+ throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse);
+ }
}
}
diff --git a/src/NzbDrone.Common/Http/HttpClient.cs b/src/NzbDrone.Common/Http/HttpClient.cs
index 849647f64..45ff53ae3 100644
--- a/src/NzbDrone.Common/Http/HttpClient.cs
+++ b/src/NzbDrone.Common/Http/HttpClient.cs
@@ -7,6 +7,7 @@ using System.Net;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
+using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.TPL;
@@ -51,46 +52,35 @@ namespace NzbDrone.Common.Http
public HttpResponse Execute(HttpRequest request)
{
- foreach (var interceptor in _requestInterceptors)
+ var cookieContainer = InitializeRequestCookies(request);
+
+ var response = ExecuteRequest(request, cookieContainer);
+
+ if (request.AllowAutoRedirect && response.HasHttpRedirect)
{
- request = interceptor.PreRequest(request);
+ var autoRedirectChain = new List();
+ autoRedirectChain.Add(request.Url.ToString());
+
+ do
+ {
+ request.Url += new HttpUri(response.Headers.GetSingleValue("Location"));
+ autoRedirectChain.Add(request.Url.ToString());
+
+ _logger.Trace("Redirected to {0}", request.Url);
+
+ if (autoRedirectChain.Count > 3)
+ {
+ throw new WebException($"Too many automatic redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError);
+ }
+
+ response = ExecuteRequest(request, cookieContainer);
+ }
+ while (response.HasHttpRedirect);
}
- if (request.RateLimit != TimeSpan.Zero)
+ if (response.HasHttpRedirect && !RuntimeInfo.IsProduction)
{
- _rateLimitService.WaitAndPulse(request.Url.Host, request.RateLimit);
- }
-
- _logger.Trace(request);
-
- var stopWatch = Stopwatch.StartNew();
-
- var cookies = PrepareRequestCookies(request);
-
- var response = _httpDispatcher.GetResponse(request, cookies);
-
- HandleResponseCookies(request, cookies);
-
- stopWatch.Stop();
-
- _logger.Trace("{0} ({1} ms)", response, stopWatch.ElapsedMilliseconds);
-
- foreach (var interceptor in _requestInterceptors)
- {
- response = interceptor.PostResponse(response);
- }
-
- if (request.LogResponseContent)
- {
- _logger.Trace("Response content ({0} bytes): {1}", response.ResponseData.Length, response.Content);
- }
-
- if (!RuntimeInfo.IsProduction &&
- (response.StatusCode == HttpStatusCode.Moved ||
- response.StatusCode == HttpStatusCode.MovedPermanently ||
- response.StatusCode == HttpStatusCode.Found))
- {
- _logger.Error("Server requested a redirect to [{0}]. Update the request URL to avoid this redirect.", response.Headers["Location"]);
+ _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
}
if (!request.SuppressHttpError && response.HasHttpError)
@@ -110,49 +100,130 @@ namespace NzbDrone.Common.Http
return response;
}
- private CookieContainer PrepareRequestCookies(HttpRequest request)
+ private HttpResponse ExecuteRequest(HttpRequest request, CookieContainer cookieContainer)
+ {
+ foreach (var interceptor in _requestInterceptors)
+ {
+ request = interceptor.PreRequest(request);
+ }
+
+ if (request.RateLimit != TimeSpan.Zero)
+ {
+ _rateLimitService.WaitAndPulse(request.Url.Host, request.RateLimit);
+ }
+
+ _logger.Trace(request);
+
+ var stopWatch = Stopwatch.StartNew();
+
+ PrepareRequestCookies(request, cookieContainer);
+
+ var response = _httpDispatcher.GetResponse(request, cookieContainer);
+
+ HandleResponseCookies(response, cookieContainer);
+
+ stopWatch.Stop();
+
+ _logger.Trace("{0} ({1} ms)", response, stopWatch.ElapsedMilliseconds);
+
+ foreach (var interceptor in _requestInterceptors)
+ {
+ response = interceptor.PostResponse(response);
+ }
+
+ if (request.LogResponseContent)
+ {
+ _logger.Trace("Response content ({0} bytes): {1}", response.ResponseData.Length, response.Content);
+ }
+
+ return response;
+ }
+
+ private CookieContainer InitializeRequestCookies(HttpRequest request)
{
lock (_cookieContainerCache)
{
- var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
+ var sourceContainer = new CookieContainer();
+
+ var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
+ var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
+ sourceContainer.Add(persistentCookies);
if (request.Cookies.Count != 0)
{
foreach (var pair in request.Cookies)
{
- persistentCookieContainer.Add(new Cookie(pair.Key, pair.Value, "/", request.Url.Host)
+ Cookie cookie;
+ if (pair.Value == null)
{
- // Use Now rather than UtcNow to work around Mono cookie expiry bug.
- // See https://gist.github.com/ta264/7822b1424f72e5b4c961
- Expires = DateTime.Now.AddHours(1)
- });
+ cookie = new Cookie(pair.Key, "", "/")
+ {
+ Expires = DateTime.Now.AddDays(-1)
+ };
+ }
+ else
+ {
+ cookie = new Cookie(pair.Key, pair.Value, "/")
+ {
+ // Use Now rather than UtcNow to work around Mono cookie expiry bug.
+ // See https://gist.github.com/ta264/7822b1424f72e5b4c961
+ Expires = DateTime.Now.AddHours(1)
+ };
+ }
+
+ sourceContainer.Add((Uri)request.Url, cookie);
+
+ if (request.StoreRequestCookie)
+ {
+ presistentContainer.Add((Uri)request.Url, cookie);
+ }
}
}
- var requestCookies = persistentCookieContainer.GetCookies((Uri)request.Url);
-
- var cookieContainer = new CookieContainer();
-
- cookieContainer.Add(requestCookies);
-
- return cookieContainer;
+ return sourceContainer;
}
}
- private void HandleResponseCookies(HttpRequest request, CookieContainer cookieContainer)
+ private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer)
{
- if (!request.StoreResponseCookie)
+ // Don't collect persistnet cookies for intermediate/redirected urls.
+ /*lock (_cookieContainerCache)
+ {
+ var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
+ var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
+ var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
+
+ cookieContainer.Add(persistentCookies);
+ cookieContainer.Add(existingCookies);
+ }*/
+ }
+
+ private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer)
+ {
+ var cookieHeaders = response.GetCookieHeaders();
+ if (cookieHeaders.Empty())
{
return;
}
- lock (_cookieContainerCache)
+ if (response.Request.StoreResponseCookie)
{
- var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
+ lock (_cookieContainerCache)
+ {
+ var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
- var cookies = cookieContainer.GetCookies((Uri)request.Url);
-
- persistentCookieContainer.Add(cookies);
+ foreach (var cookieHeader in cookieHeaders)
+ {
+ try
+ {
+ persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
+ }
+ catch (Exception ex)
+ {
+ _logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url);
+ }
+ }
+ }
}
}
@@ -217,4 +288,4 @@ namespace NzbDrone.Common.Http
return new HttpResponse(response);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Http/HttpHeader.cs b/src/NzbDrone.Common/Http/HttpHeader.cs
index 88e0ab81e..8cf25489a 100644
--- a/src/NzbDrone.Common/Http/HttpHeader.cs
+++ b/src/NzbDrone.Common/Http/HttpHeader.cs
@@ -37,7 +37,7 @@ namespace NzbDrone.Common.Http
}
if (values.Length > 1)
{
- throw new ApplicationException(string.Format("Expected {0} to occur only once.", key));
+ throw new ApplicationException($"Expected {key} to occur only once, but was {values.Join("|")}.");
}
return values[0];
@@ -54,7 +54,7 @@ namespace NzbDrone.Common.Http
return converter(value);
}
protected void SetSingleValue(string key, string value)
- {
+ {
if (value == null)
{
Remove(key);
@@ -175,4 +175,4 @@ namespace NzbDrone.Common.Http
.ToList();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Http/HttpMethod.cs b/src/NzbDrone.Common/Http/HttpMethod.cs
index 1fa33a823..c5f7b5307 100644
--- a/src/NzbDrone.Common/Http/HttpMethod.cs
+++ b/src/NzbDrone.Common/Http/HttpMethod.cs
@@ -1,13 +1,14 @@
-namespace NzbDrone.Common.Http
+namespace NzbDrone.Common.Http
{
public enum HttpMethod
{
GET,
- PUT,
POST,
- HEAD,
+ PUT,
DELETE,
+ HEAD,
+ OPTIONS,
PATCH,
- OPTIONS
+ MERGE
}
}
\ No newline at end of file
diff --git a/src/NzbDrone.Common/Http/HttpRequest.cs b/src/NzbDrone.Common/Http/HttpRequest.cs
index 8f4b4472b..301890804 100644
--- a/src/NzbDrone.Common/Http/HttpRequest.cs
+++ b/src/NzbDrone.Common/Http/HttpRequest.cs
@@ -13,8 +13,10 @@ namespace NzbDrone.Common.Http
Url = new HttpUri(url);
Headers = new HttpHeader();
AllowAutoRedirect = true;
+ StoreRequestCookie = true;
Cookies = new Dictionary();
-
+
+
if (!RuntimeInfo.IsProduction)
{
AllowAutoRedirect = false;
@@ -37,6 +39,7 @@ namespace NzbDrone.Common.Http
public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; }
public Dictionary Cookies { get; private set; }
+ public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; }
@@ -76,5 +79,12 @@ namespace NzbDrone.Common.Http
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
ContentData = encoding.GetBytes(data);
}
+
+ public void AddBasicAuthentication(string username, string password)
+ {
+ var authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}"));
+
+ Headers.Set("Authorization", "Basic " + authInfo);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Http/HttpResponse.cs b/src/NzbDrone.Common/Http/HttpResponse.cs
index dd9df22c7..e0b11b51c 100644
--- a/src/NzbDrone.Common/Http/HttpResponse.cs
+++ b/src/NzbDrone.Common/Http/HttpResponse.cs
@@ -35,7 +35,7 @@ namespace NzbDrone.Common.Http
private string _content;
- public string Content
+ public string Content
{
get
{
@@ -51,20 +51,26 @@ namespace NzbDrone.Common.Http
public bool HasHttpError => (int)StatusCode >= 400;
+ public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
+ StatusCode == HttpStatusCode.MovedPermanently ||
+ StatusCode == HttpStatusCode.Found;
+
+ public string[] GetCookieHeaders()
+ {
+ return Headers.GetValues("Set-Cookie") ?? new string[0];
+ }
+
public Dictionary GetCookies()
{
var result = new Dictionary();
- var setCookieHeaders = Headers.GetValues("Set-Cookie");
- if (setCookieHeaders != null)
+ var setCookieHeaders = GetCookieHeaders();
+ foreach (var cookie in setCookieHeaders)
{
- foreach (var cookie in setCookieHeaders)
+ var match = RegexSetCookie.Match(cookie);
+ if (match.Success)
{
- var match = RegexSetCookie.Match(cookie);
- if (match.Success)
- {
- result[match.Groups[1].Value] = match.Groups[2].Value;
- }
+ result[match.Groups[1].Value] = match.Groups[2].Value;
}
}
@@ -95,4 +101,4 @@ namespace NzbDrone.Common.Http
public T Resource { get; private set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Http/HttpUri.cs b/src/NzbDrone.Common/Http/HttpUri.cs
index 23e47be94..cbbecd718 100644
--- a/src/NzbDrone.Common/Http/HttpUri.cs
+++ b/src/NzbDrone.Common/Http/HttpUri.cs
@@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
{
public class HttpUri : IEquatable
{
- private static readonly Regex RegexUri = new Regex(@"^(?:(?[a-z]+):)?(?://(?[-A-Z0-9.]+)(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/)[^/?#\r\n]+)+/?|/)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private static readonly Regex RegexUri = new Regex(@"^(?:(?[a-z]+):)?(?://(?[-_A-Z0-9.]+)(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/)[^/?#\r\n]+)+/?|/)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly string _uri;
public string FullUri => _uri;
@@ -168,7 +168,7 @@ namespace NzbDrone.Common.Http
{
return basePath.Substring(0, baseSlashIndex) + "/" + relativePath;
}
-
+
return relativePath;
}
@@ -263,7 +263,7 @@ namespace NzbDrone.Common.Http
{
return new HttpUri(baseUrl.Scheme, baseUrl.Host, baseUrl.Port, CombineRelativePath(baseUrl.Path, relativeUrl.Path), relativeUrl.Query, relativeUrl.Fragment);
}
-
+
return new HttpUri(baseUrl.Scheme, baseUrl.Host, baseUrl.Port, baseUrl.Path, relativeUrl.Query, relativeUrl.Fragment);
}
}
diff --git a/src/NzbDrone.Common/Http/TlsFailureException.cs b/src/NzbDrone.Common/Http/TlsFailureException.cs
new file mode 100644
index 000000000..c1dcdd991
--- /dev/null
+++ b/src/NzbDrone.Common/Http/TlsFailureException.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace NzbDrone.Common.Http
+{
+ public class TlsFailureException : WebException
+ {
+ public TlsFailureException(WebRequest request, WebException innerException)
+ : base("Failed to establish secure https connection to '" + request.RequestUri + "', libcurl fallback might be unavailable.", innerException, WebExceptionStatus.SecureChannelFailure, innerException.Response)
+ {
+
+ }
+
+ }
+}
diff --git a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs
index ef33968e5..a53e8e282 100644
--- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs
+++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs
@@ -6,10 +6,10 @@ namespace NzbDrone.Common.Instrumentation
{
public class CleanseLogMessage
{
- private static readonly Regex[] CleansingRules = new[]
+ private static readonly Regex[] CleansingRules = new[]
{
// Url
- new Regex(@"(?<=\?|&)(apikey|token|passkey|auth|authkey|user|uid|api)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+ new Regex(@"(?<=\?|&)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/(?!rss)(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
diff --git a/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs b/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs
new file mode 100644
index 000000000..f33f4587b
--- /dev/null
+++ b/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Newtonsoft.Json.Linq;
+using NzbDrone.Common.Serializer;
+
+namespace NzbDrone.Common.Instrumentation
+{
+ public class CleansingJsonVisitor : JsonVisitor
+ {
+ public override void Visit(JArray json)
+ {
+ for (var i = 0; i < json.Count; i++)
+ {
+ if (json[i].Type == JTokenType.String)
+ {
+ var text = json[i].Value();
+ json[i] = new JValue(CleanseLogMessage.Cleanse(text));
+ }
+ }
+ foreach (JToken token in json)
+ {
+ Visit(token);
+ }
+ }
+
+ public override void Visit(JProperty property)
+ {
+ if (property.Value.Type == JTokenType.String)
+ {
+ property.Value = CleanseValue(property.Value as JValue);
+ }
+ else
+ {
+ base.Visit(property);
+ }
+ }
+
+ private JValue CleanseValue(JValue value)
+ {
+ var text = value.Value();
+ var cleansed = CleanseLogMessage.Cleanse(text);
+ return new JValue(cleansed);
+ }
+ }
+}
diff --git a/src/NzbDrone.Common/Instrumentation/Extensions/LoggerProgressExtensions.cs b/src/NzbDrone.Common/Instrumentation/Extensions/LoggerExtensions.cs
similarity index 98%
rename from src/NzbDrone.Common/Instrumentation/Extensions/LoggerProgressExtensions.cs
rename to src/NzbDrone.Common/Instrumentation/Extensions/LoggerExtensions.cs
index 5abeeb6ba..ca377e8f4 100644
--- a/src/NzbDrone.Common/Instrumentation/Extensions/LoggerProgressExtensions.cs
+++ b/src/NzbDrone.Common/Instrumentation/Extensions/LoggerExtensions.cs
@@ -1,4 +1,5 @@
using NLog;
+using NLog.Fluent;
namespace NzbDrone.Common.Instrumentation.Extensions
{
diff --git a/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs b/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs
new file mode 100644
index 000000000..3084d380b
--- /dev/null
+++ b/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NLog;
+using NLog.Fluent;
+
+namespace NzbDrone.Common.Instrumentation.Extensions
+{
+ public static class SentryLoggerExtensions
+ {
+ public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
+
+ public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
+ {
+ return logBuilder.Property("Sentry", fingerprint);
+ }
+
+ public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
+ {
+ return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
+ }
+
+ public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
+ {
+ return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
+ }
+
+ public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
+ {
+ return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
+ }
+
+ public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
+ {
+ return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
+ }
+
+ private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
+ {
+ SentryLogger.Log(level)
+ .CopyLogEvent(logBuilder.LogEventInfo)
+ .SentryFingerprint(fingerprint)
+ .Write();
+
+ return logBuilder.Property("Sentry", null);
+ }
+
+ private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
+ {
+ return logBuilder.LoggerName(logEvent.LoggerName)
+ .TimeStamp(logEvent.TimeStamp)
+ .Message(logEvent.Message, logEvent.Parameters)
+ .Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
+ .Exception(logEvent.Exception);
+ }
+ }
+}
diff --git a/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs b/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs
index fbcbf4dcb..722b9823f 100644
--- a/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs
+++ b/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs
@@ -31,7 +31,7 @@ namespace NzbDrone.Common.Instrumentation
if (exception is NullReferenceException &&
exception.ToString().Contains("Microsoft.AspNet.SignalR.Transports.TransportHeartbeat.ProcessServerCommand"))
{
- Logger.Warn("SignalR Heartbeat interupted");
+ Logger.Warn("SignalR Heartbeat interrupted");
return;
}
@@ -49,4 +49,4 @@ namespace NzbDrone.Common.Instrumentation
Logger.Fatal(exception, "EPIC FAIL.");
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs
index 60373b991..fa6087762 100644
--- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs
+++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs
@@ -99,7 +99,7 @@ namespace NzbDrone.Common.Instrumentation
else
{
dsn = RuntimeInfo.IsProduction
- ? "https://3e8a38b1a4df4de8b0453a724f5a1139:5a708dd75c724b32ae5128b6a895650f@sentry.sonarr.tv/8"
+ ? "https://a013727b8d224e719894e1e13ff4966b:c95ca1f9ca02418d829db10c2938baf4@sentry.sonarr.tv/8"
: "https://4ee3580e01d8407c96a7430fbc953512:5f2d07227a0b4fde99dea07041a3ff93@sentry.sonarr.tv/10";
}
@@ -109,9 +109,13 @@ namespace NzbDrone.Common.Instrumentation
Layout = "${message}"
};
- var loggingRule = new LoggingRule("*", updateClient ? LogLevel.Trace : LogLevel.Error, target);
+ var loggingRule = new LoggingRule("*", updateClient ? LogLevel.Trace : LogLevel.Warn, target);
LogManager.Configuration.AddTarget("sentryTarget", target);
LogManager.Configuration.LoggingRules.Add(loggingRule);
+
+ // Events logged to Sentry go only to Sentry.
+ var loggingRuleSentry = new LoggingRule("Sentry", LogLevel.Debug, target) { Final = true };
+ LogManager.Configuration.LoggingRules.Insert(0, loggingRuleSentry);
}
private static void RegisterDebugger()
diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryPacketCleanser.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryPacketCleanser.cs
new file mode 100644
index 000000000..0815c0d48
--- /dev/null
+++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryPacketCleanser.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Newtonsoft.Json.Linq;
+
+namespace NzbDrone.Common.Instrumentation.Sentry
+{
+ public class SentryPacketCleanser
+ {
+ public void CleansePacket(SonarrSentryPacket packet)
+ {
+ packet.Message = CleanseLogMessage.Cleanse(packet.Message);
+
+ if (packet.Fingerprint != null)
+ {
+ for (var i = 0; i < packet.Fingerprint.Length; i++)
+ {
+ packet.Fingerprint[i] = CleanseLogMessage.Cleanse(packet.Fingerprint[i]);
+ }
+ }
+
+ if (packet.Extra != null)
+ {
+ var target = JObject.FromObject(packet.Extra);
+ new CleansingJsonVisitor().Visit(target);
+ packet.Extra = target;
+ }
+ }
+ }
+}
diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs
index b0b20eeee..af629df16 100644
--- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs
+++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
@@ -71,6 +72,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
private static List GetFingerPrint(LogEventInfo logEvent)
{
+ if (logEvent.Properties.ContainsKey("Sentry"))
+ {
+ return ((string[])logEvent.Properties["Sentry"]).ToList();
+ }
+
var fingerPrint = new List
{
logEvent.Level.Ordinal.ToString(),
@@ -94,13 +100,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
return fingerPrint;
}
+ private bool IsSentryMessage(LogEventInfo logEvent)
+ {
+ if (logEvent.Properties.ContainsKey("Sentry"))
+ {
+ return logEvent.Properties["Sentry"] != null;
+ }
+
+ if (logEvent.Level >= LogLevel.Error && logEvent.Exception != null)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
protected override void Write(LogEventInfo logEvent)
{
+ if (_unauthorized)
+ {
+ return;
+ }
+
try
{
// don't report non-critical events without exceptions
- if (logEvent.Exception == null || _unauthorized)
+ if (!IsSentryMessage(logEvent))
{
return;
}
@@ -112,8 +138,16 @@ namespace NzbDrone.Common.Instrumentation.Sentry
}
var extras = logEvent.Properties.ToDictionary(x => x.Key.ToString(), x => x.Value.ToString());
+ extras.Remove("Sentry");
_client.Logger = logEvent.LoggerName;
+ if (logEvent.Exception != null)
+ {
+ foreach (DictionaryEntry data in logEvent.Exception.Data)
+ {
+ extras.Add(data.Key.ToString(), data.Value.ToString());
+ }
+ }
var sentryMessage = new SentryMessage(logEvent.Message, logEvent.Parameters);
@@ -135,11 +169,16 @@ namespace NzbDrone.Common.Instrumentation.Sentry
sentryEvent.Fingerprint.Add(logEvent.Exception.GetType().FullName);
}
+ if (logEvent.Properties.ContainsKey("Sentry"))
+ {
+ sentryEvent.Fingerprint.Clear();
+ Array.ForEach((string[])logEvent.Properties["Sentry"], sentryEvent.Fingerprint.Add);
+ }
+
var osName = Environment.GetEnvironmentVariable("OS_NAME");
var osVersion = Environment.GetEnvironmentVariable("OS_VERSION");
var runTimeVersion = Environment.GetEnvironmentVariable("RUNTIME_VERSION");
-
sentryEvent.Tags.Add("os_name", osName);
sentryEvent.Tags.Add("os_version", $"{osName} {osVersion}");
sentryEvent.Tags.Add("runtime_version", $"{PlatformInfo.PlatformName} {runTimeVersion}");
diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SonarrJsonPacketFactory.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SonarrJsonPacketFactory.cs
index fb639fb2f..3ba6b499c 100644
--- a/src/NzbDrone.Common/Instrumentation/Sentry/SonarrJsonPacketFactory.cs
+++ b/src/NzbDrone.Common/Instrumentation/Sentry/SonarrJsonPacketFactory.cs
@@ -6,6 +6,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{
public class SonarrJsonPacketFactory : IJsonPacketFactory
{
+ private readonly SentryPacketCleanser _cleanser;
+
+ public SonarrJsonPacketFactory()
+ {
+ _cleanser = new SentryPacketCleanser();
+ }
+
private static string ShortenPath(string path)
{
@@ -37,6 +44,8 @@ namespace NzbDrone.Common.Instrumentation.Sentry
frame.Filename = ShortenPath(frame.Filename);
}
}
+
+ _cleanser.CleansePacket(packet);
}
catch (Exception)
{
@@ -46,7 +55,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
return packet;
}
-
[Obsolete]
public JsonPacket Create(string project, SentryMessage message, ErrorLevel level = ErrorLevel.Info, IDictionary tags = null,
string[] fingerprint = null, object extra = null)
@@ -61,4 +69,4 @@ namespace NzbDrone.Common.Instrumentation.Sentry
throw new NotImplementedException();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj
index 34ee38755..34f7bec43 100644
--- a/src/NzbDrone.Common/NzbDrone.Common.csproj
+++ b/src/NzbDrone.Common/NzbDrone.Common.csproj
@@ -44,29 +44,30 @@
True
- ..\packages\NLog.4.4.3\lib\net40\NLog.dll
+ ..\packages\NLog.4.5.3\lib\net40-client\NLog.dll
-
- ..\packages\DotNet4.SocksProxy.1.3.2.0\lib\net40\Org.Mentalis.dll
- True
+
+ ..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll..\packages\SharpRaven.2.2.0\lib\net40\SharpRaven.dll
-
- ..\packages\DotNet4.SocksProxy.1.3.2.0\lib\net40\SocksWebProxy.dll
- True
+
+ ..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\SocksWebProxy.dll
+
+ ..\packages\ICSharpCode.SharpZipLib.Patched.0.86.5\lib\net20\ICSharpCode.SharpZipLib.dll
+
@@ -87,12 +88,15 @@
+
+
+
@@ -125,7 +129,7 @@
-
+
@@ -136,6 +140,7 @@
+
@@ -171,16 +176,20 @@
+
-
+
+
+
+
@@ -205,6 +214,8 @@
+
+
diff --git a/src/NzbDrone.Common/Processes/PidFileProvider.cs b/src/NzbDrone.Common/Processes/PidFileProvider.cs
index c04ff445f..04cca6527 100644
--- a/src/NzbDrone.Common/Processes/PidFileProvider.cs
+++ b/src/NzbDrone.Common/Processes/PidFileProvider.cs
@@ -2,6 +2,7 @@
using System.IO;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
+using NzbDrone.Common.Exceptions;
namespace NzbDrone.Common.Processes
{
@@ -38,7 +39,7 @@ namespace NzbDrone.Common.Processes
catch (Exception ex)
{
_logger.Error(ex, "Unable to write PID file {0}", filename);
- throw;
+ throw new SonarrStartupException(ex, "Unable to write PID file {0}", filename);
}
}
}
diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs
index 49e61c621..0ddab771f 100644
--- a/src/NzbDrone.Common/Processes/ProcessProvider.cs
+++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs
@@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
+using NzbDrone.Common.Extensions;
using NzbDrone.Common.Model;
namespace NzbDrone.Common.Processes
@@ -129,7 +130,25 @@ namespace NzbDrone.Common.Processes
{
foreach (DictionaryEntry environmentVariable in environmentVariables)
{
- startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
+ try
+ {
+ _logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
+ startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
+ }
+ catch (Exception e)
+ {
+ if (environmentVariable.Value == null)
+ {
+ _logger.Error(e, "Unable to set environment variable '{0}', value is null", environmentVariable.Key);
+ }
+
+ else
+ {
+ _logger.Error(e, "Unable to set environment variable '{0}'", environmentVariable.Key);
+ }
+
+ throw;
+ }
}
}
diff --git a/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs b/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs
index db7edc31b..7fbaa9ec8 100644
--- a/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs
+++ b/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs
@@ -60,6 +60,11 @@ namespace NzbDrone.Common.Reflection
return (T)attribute;
}
+ public static T[] GetAttributes(this MemberInfo member) where T : Attribute
+ {
+ return member.GetCustomAttributes(typeof(T), false).OfType().ToArray();
+ }
+
public static Type FindTypeByName(this Assembly assembly, string name)
{
return assembly.GetTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
@@ -70,4 +75,4 @@ namespace NzbDrone.Common.Reflection
return type.GetCustomAttributes(typeof(TAttribute), true).Any();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Common/Security/SecurityProtocolPolicy.cs b/src/NzbDrone.Common/Security/SecurityProtocolPolicy.cs
index 03fcb97d2..17c625ef6 100644
--- a/src/NzbDrone.Common/Security/SecurityProtocolPolicy.cs
+++ b/src/NzbDrone.Common/Security/SecurityProtocolPolicy.cs
@@ -1,6 +1,7 @@
using System;
using System.Net;
using NLog;
+using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Security
@@ -14,6 +15,12 @@ namespace NzbDrone.Common.Security
public static void Register()
{
+ if (OsInfo.IsNotWindows)
+ {
+ // This was never meant to be used on mono, and will cause issues with mono 5 and higher if btls is enabled.
+ return;
+ }
+
try
{
// TODO: In v3 we should drop support for SSL3 because its very insecure. Only leaving it enabled because some people might rely on it.
diff --git a/src/NzbDrone.Common/Serializer/JsonVisitor.cs b/src/NzbDrone.Common/Serializer/JsonVisitor.cs
new file mode 100644
index 000000000..87fdeeeec
--- /dev/null
+++ b/src/NzbDrone.Common/Serializer/JsonVisitor.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Newtonsoft.Json.Linq;
+
+namespace NzbDrone.Common.Serializer
+{
+
+ public class JsonVisitor
+ {
+ protected void Dispatch(JToken json)
+ {
+ switch (json.Type)
+ {
+ case JTokenType.Object:
+ Visit(json as JObject);
+ break;
+
+ case JTokenType.Array:
+ Visit(json as JArray);
+ break;
+
+ case JTokenType.Raw:
+ Visit(json as JRaw);
+ break;
+
+ case JTokenType.Constructor:
+ Visit(json as JConstructor);
+ break;
+
+ case JTokenType.Property:
+ Visit(json as JProperty);
+ break;
+
+ case JTokenType.Comment:
+ case JTokenType.Integer:
+ case JTokenType.Float:
+ case JTokenType.String:
+ case JTokenType.Boolean:
+ case JTokenType.Null:
+ case JTokenType.Undefined:
+ case JTokenType.Date:
+ case JTokenType.Bytes:
+ case JTokenType.Guid:
+ case JTokenType.Uri:
+ case JTokenType.TimeSpan:
+ Visit(json as JValue);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ public virtual void Visit(JToken json)
+ {
+ Dispatch(json);
+ }
+
+ public virtual void Visit(JContainer json)
+ {
+ Dispatch(json);
+ }
+
+ public virtual void Visit(JArray json)
+ {
+ foreach (JToken token in json)
+ {
+ Visit(token);
+ }
+ }
+ public virtual void Visit(JConstructor json)
+ {
+ }
+
+ public virtual void Visit(JObject json)
+ {
+ foreach (JProperty property in json.Properties())
+ {
+ Visit(property);
+ }
+ }
+
+ public virtual void Visit(JProperty property)
+ {
+ Visit(property.Value);
+ }
+
+ public virtual void Visit(JValue value)
+ {
+
+ }
+ }
+}
diff --git a/src/NzbDrone.Common/Serializer/UnderscoreStringEnumConverter.cs b/src/NzbDrone.Common/Serializer/UnderscoreStringEnumConverter.cs
new file mode 100644
index 000000000..9022c029f
--- /dev/null
+++ b/src/NzbDrone.Common/Serializer/UnderscoreStringEnumConverter.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Text;
+using Newtonsoft.Json;
+
+namespace NzbDrone.Common.Serializer
+{
+ public class UnderscoreStringEnumConverter : JsonConverter
+ {
+ public object UnknownValue { get; set; }
+
+ public UnderscoreStringEnumConverter(object unknownValue)
+ {
+ UnknownValue = unknownValue;
+ }
+
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType.IsEnum;
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ var enumString = reader.Value.ToString().Replace("_", string.Empty);
+
+ try
+ {
+ return Enum.Parse(objectType, enumString, true);
+ }
+ catch
+ {
+ if (UnknownValue == null)
+ {
+ throw;
+ }
+
+ return UnknownValue;
+ }
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ var enumText = value.ToString();
+ var builder = new StringBuilder(enumText.Length + 4);
+ builder.Append(char.ToLower(enumText[0]));
+ for (int i = 1; i < enumText.Length; i++)
+ {
+ if (char.IsUpper(enumText[i]))
+ {
+ builder.Append('_');
+ }
+ builder.Append(char.ToLower(enumText[i]));
+ }
+ enumText = builder.ToString();
+
+ writer.WriteValue(enumText);
+ }
+ }
+}
diff --git a/src/NzbDrone.Common/packages.config b/src/NzbDrone.Common/packages.config
index c3f5e4d62..c1e8d1e7b 100644
--- a/src/NzbDrone.Common/packages.config
+++ b/src/NzbDrone.Common/packages.config
@@ -1,8 +1,8 @@
-
+
-
+
\ No newline at end of file
diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs
index 6f935887f..39316bcd1 100644
--- a/src/NzbDrone.Console/ConsoleApp.cs
+++ b/src/NzbDrone.Console/ConsoleApp.cs
@@ -2,6 +2,7 @@
using System.Net.Sockets;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
+using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Host;
@@ -11,38 +12,83 @@ namespace NzbDrone.Console
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(ConsoleApp));
+ private enum ExitCodes : int
+ {
+ Normal = 0,
+ UnknownFailure = 1,
+ RecoverableFailure = 2,
+ NonRecoverableFailure = 3
+ }
+
public static void Main(string[] args)
{
try
{
var startupArgs = new StartupContext(args);
- NzbDroneLogger.Register(startupArgs, false, true);
+ try
+ {
+ NzbDroneLogger.Register(startupArgs, false, true);
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine("NLog Exception: " + ex.ToString());
+ throw;
+ }
Bootstrap.Start(startupArgs, new ConsoleAlerts());
}
- catch (SocketException exception)
+ catch (SonarrStartupException ex)
{
System.Console.WriteLine("");
System.Console.WriteLine("");
- Logger.Fatal(exception.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions");
- System.Console.WriteLine("Press enter to exit...");
- System.Console.ReadLine();
- Environment.Exit(1);
+ Logger.Fatal(ex, "EPIC FAIL!");
+ Exit(ExitCodes.NonRecoverableFailure);
}
- catch (Exception e)
+ catch (SocketException ex)
{
System.Console.WriteLine("");
System.Console.WriteLine("");
- Logger.Fatal(e, "EPIC FAIL!");
- System.Console.WriteLine("Press enter to exit...");
- System.Console.ReadLine();
- Environment.Exit(1);
+ Logger.Fatal(ex.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions");
+ Exit(ExitCodes.RecoverableFailure);
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine("");
+ System.Console.WriteLine("");
+ Logger.Fatal(ex, "EPIC FAIL!");
+ Exit(ExitCodes.UnknownFailure);
}
Logger.Info("Exiting main.");
- //Need this to terminate on mono (thanks nlog)
- LogManager.Configuration = null;
- Environment.Exit(0);
+ Exit(ExitCodes.Normal);
+ }
+
+ private static void Exit(ExitCodes exitCode)
+ {
+ LogManager.Shutdown();
+
+ if (exitCode != ExitCodes.Normal)
+ {
+ System.Console.WriteLine("Press enter to exit...");
+
+ System.Threading.Thread.Sleep(1000);
+
+ if (exitCode == ExitCodes.NonRecoverableFailure)
+ {
+ System.Console.WriteLine("Non-recoverable failure, waiting for user intervention...");
+ for (int i = 0; i < 3600; i++)
+ {
+ System.Threading.Thread.Sleep(1000);
+
+ if (System.Console.KeyAvailable) break;
+ }
+ }
+
+ // Please note that ReadLine silently succeeds if there is no console, KeyAvailable does not.
+ System.Console.ReadLine();
+ }
+
+ Environment.Exit((int)exitCode);
}
}
}
diff --git a/src/NzbDrone.Console/NzbDrone.Console.csproj b/src/NzbDrone.Console/NzbDrone.Console.csproj
index 516b4fb2b..0b78bfdf8 100644
--- a/src/NzbDrone.Console/NzbDrone.Console.csproj
+++ b/src/NzbDrone.Console/NzbDrone.Console.csproj
@@ -66,6 +66,7 @@
app.manifest
+ False..\packages\Microsoft.Owin.2.1.0\lib\net40\Microsoft.Owin.dll
@@ -79,13 +80,19 @@
True
- ..\packages\NLog.4.4.3\lib\net40\NLog.dll
+ ..\packages\NLog.4.5.3\lib\net40-client\NLog.dll
+ ..\packages\Owin.1.0\lib\net40\Owin.dll
+
+
+
+
+
diff --git a/src/NzbDrone.Console/packages.config b/src/NzbDrone.Console/packages.config
index 11b77285e..f3188f1a4 100644
--- a/src/NzbDrone.Console/packages.config
+++ b/src/NzbDrone.Console/packages.config
@@ -3,6 +3,6 @@
-
+
\ No newline at end of file
diff --git a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs
index b94578c32..1f1a6ff8e 100644
--- a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs
+++ b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs
@@ -20,11 +20,14 @@ namespace NzbDrone.Core.Test.DataAugmentation.Scene
private Mock _provider1;
private Mock _provider2;
-
+
[SetUp]
public void Setup()
{
- _fakeMappings = Builder.CreateListOfSize(5).BuildListOfNew();
+ _fakeMappings = Builder.CreateListOfSize(5)
+ .All()
+ .With(v => v.FilterRegex = null)
+ .BuildListOfNew();
_fakeMappings[0].SearchTerm = "Words";
_fakeMappings[1].SearchTerm = "That";
@@ -193,7 +196,7 @@ namespace NzbDrone.Core.Test.DataAugmentation.Scene
Mocker.GetMock().Setup(c => c.All()).Returns(mappings);
var tvdbId = Subject.FindTvdbId(parseTitle);
- var seasonNumber = Subject.GetSceneSeasonNumber(parseTitle);
+ var seasonNumber = Subject.GetSceneSeasonNumber(parseTitle, null);
tvdbId.Should().Be(100);
seasonNumber.Should().Be(expectedSeasonNumber);
@@ -314,6 +317,49 @@ namespace NzbDrone.Core.Test.DataAugmentation.Scene
Subject.GetSceneNames(100, new List { 4 }, new List { 4 }).Should().BeEmpty();
}
+ [Test]
+ public void should_filter_by_regex()
+ {
+ var mappings = new List
+ {
+ new SceneMapping { Title = "Amareto", ParseTerm = "amareto", SearchTerm = "Amareto", TvdbId = 100 },
+ new SceneMapping { Title = "Amareto", ParseTerm = "amareto", SearchTerm = "Amareto", TvdbId = 101, FilterRegex="-Viva$" }
+ };
+
+ Mocker.GetMock().Setup(c => c.All()).Returns(mappings);
+
+ Subject.FindTvdbId("Amareto", "Amareto.S01E01.720p.WEB-DL-Viva").Should().Be(101);
+ Subject.FindTvdbId("Amareto", "Amareto.S01E01.720p.WEB-DL-DMO").Should().Be(100);
+ }
+
+ [Test]
+ public void should_throw_if_multiple_mappings()
+ {
+ var mappings = new List
+ {
+ new SceneMapping { Title = "Amareto", ParseTerm = "amareto", SearchTerm = "Amareto", TvdbId = 100 },
+ new SceneMapping { Title = "Amareto", ParseTerm = "amareto", SearchTerm = "Amareto", TvdbId = 101 }
+ };
+
+ Mocker.GetMock().Setup(c => c.All()).Returns(mappings);
+
+ Assert.Throws(() => Subject.FindTvdbId("Amareto", "Amareto.S01E01.720p.WEB-DL-Viva"));
+ }
+
+ [Test]
+ public void should_not_throw_if_multiple_mappings_with_same_tvdbid()
+ {
+ var mappings = new List
+ {
+ new SceneMapping { Title = "Amareto", ParseTerm = "amareto", SearchTerm = "Amareto", TvdbId = 100 },
+ new SceneMapping { Title = "Amareto", ParseTerm = "amareto", SearchTerm = "Amareto", TvdbId = 100 }
+ };
+
+ Mocker.GetMock().Setup(c => c.All()).Returns(mappings);
+
+ Subject.FindTvdbId("Amareto", "Amareto.S01E01.720p.WEB-DL-Viva").Should().Be(100);
+ }
+
private void AssertNoUpdate()
{
_provider1.Verify(c => c.GetSceneMappings(), Times.Once());
diff --git a/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs b/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs
index 3f263c6dd..99f522a79 100644
--- a/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs
+++ b/src/NzbDrone.Core.Test/DataAugmentation/SceneNumbering/XemServiceFixture.cs
@@ -5,6 +5,7 @@ using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
+using NzbDrone.Common.Extensions;
using NzbDrone.Core.DataAugmentation.Xem;
using NzbDrone.Core.DataAugmentation.Xem.Model;
using NzbDrone.Core.Test.Framework;
@@ -98,7 +99,6 @@ namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering
});
}
-
[Test]
public void should_not_fetch_scenenumbering_if_not_listed()
{
@@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering
.Verify(v => v.GetSceneTvdbMappings(10), Times.Never());
Mocker.GetMock()
- .Verify(v => v.UpdateSeries(It.IsAny()), Times.Never());
+ .Verify(v => v.UpdateSeries(It.IsAny(), It.IsAny()), Times.Never());
}
[Test]
@@ -119,7 +119,7 @@ namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering
Subject.Handle(new SeriesUpdatedEvent(_series));
Mocker.GetMock()
- .Verify(v => v.UpdateSeries(It.Is(s => s.UseSceneNumbering == true)), Times.Once());
+ .Verify(v => v.UpdateSeries(It.Is(s => s.UseSceneNumbering == true), It.IsAny()), Times.Once());
}
[Test]
@@ -130,7 +130,7 @@ namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering
Subject.Handle(new SeriesUpdatedEvent(_series));
Mocker.GetMock()
- .Verify(v => v.UpdateSeries(It.IsAny()), Times.Once());
+ .Verify(v => v.UpdateSeries(It.IsAny(), It.IsAny()), Times.Once());
}
[Test]
@@ -143,7 +143,7 @@ namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering
Subject.Handle(new SeriesUpdatedEvent(_series));
Mocker.GetMock()
- .Verify(v => v.UpdateSeries(It.IsAny()), Times.Never());
+ .Verify(v => v.UpdateSeries(It.IsAny(), It.IsAny()), Times.Never());
ExceptionVerification.ExpectedWarns(1);
}
@@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering
Subject.Handle(new SeriesUpdatedEvent(_series));
Mocker.GetMock()
- .Verify(v => v.UpdateSeries(It.IsAny()), Times.Never());
+ .Verify(v => v.UpdateSeries(It.IsAny(), It.IsAny()), Times.Never());
ExceptionVerification.ExpectedWarns(1);
}
@@ -308,5 +308,19 @@ namespace NzbDrone.Core.Test.DataAugmentation.SceneNumbering
episode.SceneSeasonNumber.Should().NotHaveValue();
episode.SceneEpisodeNumber.Should().NotHaveValue();
}
+
+ [Test]
+ public void should_skip_mapping_when_scene_information_is_all_zero()
+ {
+ GivenTvdbMappings();
+
+ AddTvdbMapping(0, 0, 0, 8, 3, 1); // 3x01 -> 3x01
+ AddTvdbMapping(0, 0, 0, 9, 3, 2); // 3x02 -> 3x02
+
+ Subject.Handle(new SeriesUpdatedEvent(_series));
+
+ Mocker.GetMock()
+ .Verify(v => v.UpdateEpisodes(It.Is>(e => e.Any(c => c.SceneAbsoluteEpisodeNumber == 0 && c.SceneSeasonNumber == 0 && c.SceneEpisodeNumber == 0))), Times.Never());
+ }
}
}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/BooleanIntConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/BooleanIntConverterFixture.cs
new file mode 100644
index 000000000..649a7303e
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/BooleanIntConverterFixture.cs
@@ -0,0 +1,59 @@
+using System;
+using FluentAssertions;
+using Marr.Data.Converters;
+using NUnit.Framework;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class BooleanIntConverterFixture : CoreTest
+ {
+ [TestCase(true, 1)]
+ [TestCase(false, 0)]
+ public void should_return_int_when_saving_boolean_to_db(bool input, int expected)
+ {
+ Subject.ToDB(input).Should().Be(expected);
+ }
+
+ [Test]
+ public void should_return_db_null_for_null_value_when_saving_to_db()
+ {
+ Subject.ToDB(null).Should().Be(DBNull.Value);
+ }
+
+ [TestCase(1, true)]
+ [TestCase(0, false)]
+ public void should_return_bool_when_getting_int_from_db(int input, bool expected)
+ {
+ var context = new ConverterContext
+ {
+ DbValue = (long)input
+ };
+
+ Subject.FromDB(context).Should().Be(expected);
+ }
+
+ [Test]
+ public void should_return_db_null_for_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(DBNull.Value);
+ }
+
+ [Test]
+ public void should_throw_for_non_boolean_equivalent_number_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = (long)2
+ };
+
+ Assert.Throws(() => Subject.FromDB(context));
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/CommandConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/CommandConverterFixture.cs
new file mode 100644
index 000000000..ac0a3359e
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/CommandConverterFixture.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Data;
+using FluentAssertions;
+using Marr.Data.Converters;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Common.Serializer;
+using NzbDrone.Core.Datastore.Converters;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Core.Tv.Commands;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class CommandConverterFixture : CoreTest
+ {
+ [Test]
+ public void should_return_json_string_when_saving_boolean_to_db()
+ {
+ var command = new RefreshSeriesCommand();
+
+ Subject.ToDB(command).Should().BeOfType();
+ }
+
+ [Test]
+ public void should_return_null_for_null_value_when_saving_to_db()
+ {
+ Subject.ToDB(null).Should().Be(null);
+ }
+
+ [Test]
+ public void should_return_db_null_for_db_null_value_when_saving_to_db()
+ {
+ Subject.ToDB(DBNull.Value).Should().Be(DBNull.Value);
+ }
+
+ [Test]
+ public void should_return_command_when_getting_json_from_db()
+ {
+ var dataRecordMock = new Mock();
+ dataRecordMock.Setup(s => s.GetOrdinal("Name")).Returns(0);
+ dataRecordMock.Setup(s => s.GetString(0)).Returns("RefreshSeries");
+
+ var context = new ConverterContext
+ {
+ DataRecord = dataRecordMock.Object,
+ DbValue = new RefreshSeriesCommand().ToJson()
+ };
+
+ Subject.FromDB(context).Should().BeOfType();
+ }
+
+ [Test]
+ public void should_return_null_for_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(null);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/DoubleConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/DoubleConverterFixture.cs
new file mode 100644
index 000000000..bf4974124
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/DoubleConverterFixture.cs
@@ -0,0 +1,70 @@
+using System;
+using FluentAssertions;
+using Marr.Data.Converters;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore.Converters;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class DoubleConverterFixture : CoreTest
+ {
+ [Test]
+ public void should_return_double_when_saving_double_to_db()
+ {
+ var input = 10.5D;
+
+ Subject.ToDB(input).Should().Be(input);
+ }
+
+ [Test]
+ public void should_return_null_for_null_value_when_saving_to_db()
+ {
+ Subject.ToDB(null).Should().Be(null);
+ }
+
+ [Test]
+ public void should_return_db_null_for_db_null_value_when_saving_to_db()
+ {
+ Subject.ToDB(DBNull.Value).Should().Be(DBNull.Value);
+ }
+
+ [Test]
+ public void should_return_double_when_getting_double_from_db()
+ {
+ var expected = 10.5D;
+
+ var context = new ConverterContext
+ {
+ DbValue = expected
+ };
+
+ Subject.FromDB(context).Should().Be(expected);
+ }
+
+ [Test]
+ public void should_return_double_when_getting_string_from_db()
+ {
+ var expected = 10.5D;
+
+ var context = new ConverterContext
+ {
+ DbValue = $"{expected}"
+ };
+
+ Subject.FromDB(context).Should().Be(expected);
+ }
+
+ [Test]
+ public void should_return_null_for_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(DBNull.Value);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/EnumIntConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/EnumIntConverterFixture.cs
new file mode 100644
index 000000000..d675086fb
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/EnumIntConverterFixture.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Reflection;
+using FluentAssertions;
+using Marr.Data.Converters;
+using Marr.Data.Mapping;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Core.Tv;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class EnumIntConverterFixture : CoreTest
+ {
+ [Test]
+ public void should_return_int_when_saving_enum_to_db()
+ {
+ Subject.ToDB(SeriesTypes.Standard).Should().Be((int)SeriesTypes.Standard);
+ }
+
+ [Test]
+ public void should_return_db_null_for_null_value_when_saving_to_db()
+ {
+ Subject.ToDB(null).Should().Be(DBNull.Value);
+ }
+
+ [Test]
+ public void should_return_enum_when_getting_int_from_db()
+ {
+ var mockMemberInfo = new Mock();
+ mockMemberInfo.SetupGet(s => s.DeclaringType).Returns(typeof(Series));
+ mockMemberInfo.SetupGet(s => s.Name).Returns("SeriesType");
+
+ var expected = SeriesTypes.Standard;
+
+ var context = new ConverterContext
+ {
+ ColumnMap = new ColumnMap(mockMemberInfo.Object) { FieldType = typeof(SeriesTypes) },
+ DbValue = (long)expected
+ };
+
+ Subject.FromDB(context).Should().Be(expected);
+ }
+
+ [Test]
+ public void should_return_null_for_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(null);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/GuidConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/GuidConverterFixture.cs
new file mode 100644
index 000000000..8444fa053
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/GuidConverterFixture.cs
@@ -0,0 +1,51 @@
+using System;
+using FluentAssertions;
+using Marr.Data.Converters;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore.Converters;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class GuidConverterFixture : CoreTest
+ {
+ [Test]
+ public void should_return_string_when_saving_guid_to_db()
+ {
+ var guid = Guid.NewGuid();
+
+ Subject.ToDB(guid).Should().Be(guid.ToString());
+ }
+
+ [Test]
+ public void should_return_db_null_for_null_value_when_saving_to_db()
+ {
+ Subject.ToDB(null).Should().Be(DBNull.Value);
+ }
+
+ [Test]
+ public void should_return_guid_when_getting_string_from_db()
+ {
+ var guid = Guid.NewGuid();
+
+ var context = new ConverterContext
+ {
+ DbValue = guid.ToString()
+ };
+
+ Subject.FromDB(context).Should().Be(guid);
+ }
+
+ [Test]
+ public void should_return_empty_guid_for_db_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(Guid.Empty);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/Int32ConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/Int32ConverterFixture.cs
new file mode 100644
index 000000000..2c10a0aaf
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/Int32ConverterFixture.cs
@@ -0,0 +1,58 @@
+using System;
+using FluentAssertions;
+using Marr.Data.Converters;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore.Converters;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class Int32ConverterFixture : CoreTest
+ {
+ [Test]
+ public void should_return_int_when_saving_int_to_db()
+ {
+ var i = 5;
+
+ Subject.ToDB(5).Should().Be(5);
+ }
+
+ [Test]
+ public void should_return_int_when_getting_int_from_db()
+ {
+ var i = 5;
+
+ var context = new ConverterContext
+ {
+ DbValue = i
+ };
+
+ Subject.FromDB(context).Should().Be(i);
+ }
+
+ [Test]
+ public void should_return_int_when_getting_string_from_db()
+ {
+ var i = 5;
+
+ var context = new ConverterContext
+ {
+ DbValue = i.ToString()
+ };
+
+ Subject.FromDB(context).Should().Be(i);
+ }
+
+ [Test]
+ public void should_return_db_null_for_db_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(DBNull.Value);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/OsPathConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/OsPathConverterFixture.cs
new file mode 100644
index 000000000..f7f3da0d8
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/OsPathConverterFixture.cs
@@ -0,0 +1,49 @@
+using System;
+using FluentAssertions;
+using Marr.Data.Converters;
+using NUnit.Framework;
+using NzbDrone.Common.Disk;
+using NzbDrone.Core.Datastore.Converters;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Test.Common;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class OsPathConverterFixture : CoreTest
+ {
+ [Test]
+ public void should_return_string_when_saving_os_path_to_db()
+ {
+ var path = @"C:\Test\TV".AsOsAgnostic();
+ var osPath = new OsPath(path);
+
+ Subject.ToDB(osPath).Should().Be(path);
+ }
+
+ [Test]
+ public void should_return_os_path_when_getting_string_from_db()
+ {
+ var path = @"C:\Test\TV".AsOsAgnostic();
+ var osPath = new OsPath(path);
+
+ var context = new ConverterContext
+ {
+ DbValue = path
+ };
+
+ Subject.FromDB(context).Should().Be(osPath);
+ }
+
+ [Test]
+ public void should_return_db_null_for_db_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(DBNull.Value);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/QualityIntConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/QualityIntConverterFixture.cs
new file mode 100644
index 000000000..31ae8b6b3
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/QualityIntConverterFixture.cs
@@ -0,0 +1,58 @@
+using System;
+using FluentAssertions;
+using Marr.Data.Converters;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore.Converters;
+using NzbDrone.Core.Qualities;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class QualityIntConverterFixture : CoreTest
+ {
+ [Test]
+ public void should_return_int_when_saving_quality_to_db()
+ {
+ var quality = Quality.Bluray1080p;
+
+ Subject.ToDB(quality).Should().Be(quality.Id);
+ }
+
+ [Test]
+ public void should_return_0_when_saving_db_null_to_db()
+ {
+ Subject.ToDB(DBNull.Value).Should().Be(0);
+ }
+
+ [Test]
+ public void should_throw_when_saving_another_object_to_db()
+ {
+ Assert.Throws(() => Subject.ToDB("Not a quality"));
+ }
+
+ [Test]
+ public void should_return_quality_when_getting_string_from_db()
+ {
+ var quality = Quality.Bluray1080p;
+
+ var context = new ConverterContext
+ {
+ DbValue = quality.Id
+ };
+
+ Subject.FromDB(context).Should().Be(quality);
+ }
+
+ [Test]
+ public void should_return_db_null_for_db_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(Quality.Unknown);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs
new file mode 100644
index 000000000..c96848179
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Globalization;
+using FluentAssertions;
+using Marr.Data.Converters;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore.Converters;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class TimeSpanConverterFixture : CoreTest
+ {
+ [Test]
+ public void should_return_string_when_saving_timespan_to_db()
+ {
+ var timeSpan = TimeSpan.FromMinutes(5);
+
+ Subject.ToDB(timeSpan).Should().Be(timeSpan.ToString("c", CultureInfo.InvariantCulture));
+ }
+
+ [Test]
+ public void should_return_null_when_saving_empty_string_to_db()
+ {
+ Subject.ToDB("").Should().Be(null);
+ }
+
+ [Test]
+ public void should_return_time_span_when_getting_time_span_from_db()
+ {
+ var timeSpan = TimeSpan.FromMinutes(5);
+
+ var context = new ConverterContext
+ {
+ DbValue = timeSpan
+ };
+
+ Subject.FromDB(context).Should().Be(timeSpan);
+ }
+
+ [Test]
+ public void should_return_time_span_when_getting_string_from_db()
+ {
+ var timeSpan = TimeSpan.FromMinutes(5);
+
+ var context = new ConverterContext
+ {
+ DbValue = timeSpan.ToString("c", CultureInfo.InvariantCulture)
+ };
+
+ Subject.FromDB(context).Should().Be(timeSpan);
+ }
+
+ [Test]
+ public void should_return_time_span_zero_for_db_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(TimeSpan.Zero);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/UtcConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/UtcConverterFixture.cs
new file mode 100644
index 000000000..904f653d3
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Converters/UtcConverterFixture.cs
@@ -0,0 +1,51 @@
+using System;
+using FluentAssertions;
+using Marr.Data.Converters;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore.Converters;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Datastore.Converters
+{
+ [TestFixture]
+ public class UtcConverterFixture : CoreTest
+ {
+ [Test]
+ public void should_return_date_time_when_saving_date_time_to_db()
+ {
+ var dateTime = DateTime.Now;
+
+ Subject.ToDB(dateTime).Should().Be(dateTime.ToUniversalTime());
+ }
+
+ [Test]
+ public void should_return_db_null_when_saving_db_null_to_db()
+ {
+ Subject.ToDB(DBNull.Value).Should().Be(DBNull.Value);
+ }
+
+ [Test]
+ public void should_return_time_span_when_getting_time_span_from_db()
+ {
+ var dateTime = DateTime.Now.ToUniversalTime();
+
+ var context = new ConverterContext
+ {
+ DbValue = dateTime
+ };
+
+ Subject.FromDB(context).Should().Be(dateTime);
+ }
+
+ [Test]
+ public void should_return_db_null_for_db_null_value_when_getting_from_db()
+ {
+ var context = new ConverterContext
+ {
+ DbValue = DBNull.Value
+ };
+
+ Subject.FromDB(context).Should().Be(DBNull.Value);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/121_update_animetosho_urlFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/121_update_animetosho_urlFixture.cs
new file mode 100644
index 000000000..33bdddddd
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Migration/121_update_animetosho_urlFixture.cs
@@ -0,0 +1,41 @@
+using System.Linq;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Common.Serializer;
+using NzbDrone.Core.Datastore.Migration;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Datastore.Migration
+{
+ [TestFixture]
+ public class update_animetosho_urlFixture : MigrationTest
+ {
+ [TestCase("Newznab", "https://animetosho.org")]
+ [TestCase("Newznab", "http://animetosho.org")]
+ [TestCase("Torznab", "https://animetosho.org")]
+ [TestCase("Torznab", "http://animetosho.org")]
+ public void should_replace_old_url(string impl, string baseUrl)
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("Indexers").Row(new
+ {
+ Name = "AnimeTosho",
+ Implementation = impl,
+ Settings = new NewznabSettings121
+ {
+ BaseUrl = baseUrl,
+ ApiPath = "/feed/nabapi"
+
+ }.ToJson(),
+ ConfigContract = impl + "Settings"
+ });
+ });
+
+ var items = db.Query("SELECT * FROM Indexers");
+
+ items.Should().HaveCount(1);
+ items.First().Settings.ToObject().BaseUrl.Should().Be(baseUrl.Replace("animetosho", "feed.animetosho"));
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/BlockedIndexerSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/BlockedIndexerSpecificationFixture.cs
new file mode 100644
index 000000000..7af962a96
--- /dev/null
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/BlockedIndexerSpecificationFixture.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Core.DecisionEngine;
+using NzbDrone.Core.DecisionEngine.Specifications;
+using NzbDrone.Core.Indexers;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.DecisionEngineTests
+{
+ [TestFixture]
+
+ public class BlockedIndexerSpecificationFixture : CoreTest
+ {
+ private RemoteEpisode _remoteEpisode;
+
+ [SetUp]
+ public void Setup()
+ {
+ _remoteEpisode = new RemoteEpisode
+ {
+ Release = new ReleaseInfo { IndexerId = 1 }
+ };
+
+ Mocker.GetMock()
+ .Setup(v => v.GetBlockedProviders())
+ .Returns(new List());
+ }
+
+ private void WithBlockedIndexer()
+ {
+ Mocker.GetMock()
+ .Setup(v => v.GetBlockedProviders())
+ .Returns(new List { new IndexerStatus { ProviderId = 1, DisabledTill = DateTime.UtcNow } });
+ }
+
+ [Test]
+ public void should_return_true_if_no_blocked_indexer()
+ {
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_false_if_blocked_indexer()
+ {
+ WithBlockedIndexer();
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
+ Subject.Type.Should().Be(RejectionType.Temporary);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs
index 0206abbd2..c9225540f 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs
@@ -28,6 +28,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
private Mock _fail2;
private Mock _fail3;
+ private Mock _failDelayed1;
+
[SetUp]
public void Setup()
{
@@ -39,14 +41,19 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_fail2 = new Mock();
_fail3 = new Mock();
+ _failDelayed1 = new Mock();
+
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Accept);
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Accept);
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Accept);
-
+
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("fail1"));
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("fail2"));
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("fail3"));
+ _failDelayed1.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("failDelayed1"));
+ _failDelayed1.SetupGet(c => c.Priority).Returns(SpecificationPriority.Disk);
+
_reports = new List { new ReleaseInfo { Title = "The.Office.S03E115.DVDRip.XviD-OSiTV" } };
_remoteEpisode = new RemoteEpisode {
Series = new Series(),
@@ -78,6 +85,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_pass3.Verify(c => c.IsSatisfiedBy(_remoteEpisode, null), Times.Once());
}
+ [Test]
+ public void should_call_delayed_specifications_if_non_delayed_passed()
+ {
+ GivenSpecifications(_pass1, _failDelayed1);
+
+ Subject.GetRssDecision(_reports).ToList();
+ _failDelayed1.Verify(c => c.IsSatisfiedBy(_remoteEpisode, null), Times.Once());
+ }
+
+ [Test]
+ public void should_not_call_delayed_specifications_if_non_delayed_failed()
+ {
+ GivenSpecifications(_fail1, _failDelayed1);
+
+ Subject.GetRssDecision(_reports).ToList();
+
+ _failDelayed1.Verify(c => c.IsSatisfiedBy(_remoteEpisode, null), Times.Never());
+ }
+
[Test]
public void should_return_rejected_if_single_specs_fail()
{
@@ -214,10 +240,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var criteria = new SeasonSearchCriteria { Episodes = episodes.Take(1).ToList(), SeasonNumber = 1 };
- var reports = episodes.Select(v =>
- new ReleaseInfo()
- {
- Title = string.Format("{0}.S{1:00}E{2:00}.720p.WEB-DL-DRONE", series.Title, v.SceneSeasonNumber, v.SceneEpisodeNumber)
+ var reports = episodes.Select(v =>
+ new ReleaseInfo()
+ {
+ Title = string.Format("{0}.S{1:00}E{2:00}.720p.WEB-DL-DRONE", series.Title, v.SceneSeasonNumber, v.SceneEpisodeNumber)
}).ToList();
Mocker.GetMock()
@@ -289,4 +315,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
ExceptionVerification.ExpectedErrors(1);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/FullSeasonSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/FullSeasonSpecificationFixture.cs
index 6a66d957d..a751d2a6a 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/FullSeasonSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/FullSeasonSpecificationFixture.cs
@@ -49,27 +49,27 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
_remoteEpisode.ParsedEpisodeInfo.FullSeason = false;
_remoteEpisode.Episodes.Last().AirDateUtc = DateTime.UtcNow.AddDays(+2);
- Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_if_all_episodes_have_aired()
{
- Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_false_if_one_episode_has_not_aired()
{
_remoteEpisode.Episodes.Last().AirDateUtc = DateTime.UtcNow.AddDays(+2);
- Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
[Test]
public void should_return_false_if_an_episode_does_not_have_an_air_date()
{
_remoteEpisode.Episodes.Last().AirDateUtc = null;
- Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
}
}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/MaximumSizeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/MaximumSizeSpecificationFixture.cs
new file mode 100644
index 000000000..1b8d7ab37
--- /dev/null
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/MaximumSizeSpecificationFixture.cs
@@ -0,0 +1,75 @@
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Core.Configuration;
+using NzbDrone.Core.DecisionEngine.Specifications;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.DecisionEngineTests
+{
+ public class MaximumSizeSpecificationFixture : CoreTest
+ {
+ private RemoteEpisode _remoteEpisode;
+
+ [SetUp]
+ public void Setup()
+ {
+ _remoteEpisode = new RemoteEpisode() { Release = new ReleaseInfo() };
+ }
+
+ private void WithMaximumSize(int size)
+ {
+ Mocker.GetMock().SetupGet(c => c.MaximumSize).Returns(size);
+ }
+
+ private void WithSize(int size)
+ {
+ _remoteEpisode.Release.Size = size * 1024 * 1024;
+ }
+
+ [Test]
+ public void should_return_true_when_maximum_size_is_set_to_zero()
+ {
+ WithMaximumSize(0);
+ WithSize(1000);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_when_size_is_smaller_than_maximum_size()
+ {
+ WithMaximumSize(2000);
+ WithSize(1999);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_when_size_is_equals_to_maximum_size()
+ {
+ WithMaximumSize(2000);
+ WithSize(2000);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_false_when_size_is_bigger_than_maximum_size()
+ {
+ WithMaximumSize(2000);
+ WithSize(2001);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_return_true_when_size_is_zero()
+ {
+ WithMaximumSize(2000);
+ WithSize(0);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredEpisodeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredEpisodeSpecificationFixture.cs
index 36a46337d..f7208a2c7 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredEpisodeSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredEpisodeSpecificationFixture.cs
@@ -137,5 +137,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
WithFirstEpisodeUnmonitored();
_monitoredEpisodeSpecification.IsSatisfiedBy(_parseResultSingle, new SingleEpisodeSearchCriteria{ MonitoredEpisodesOnly = true}).Accepted.Should().BeFalse();
}
+
+ [Test]
+ public void should_return_false_if_all_episodes_are_not_monitored_for_season_pack_release()
+ {
+ WithSecondEpisodeUnmonitored();
+ _parseResultMulti.ParsedEpisodeInfo = new ParsedEpisodeInfo
+ {
+ FullSeason = true
+ };
+
+ _monitoredEpisodeSpecification.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeFalse();
+ }
}
}
\ No newline at end of file
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RawDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RawDiskSpecificationFixture.cs
index 024c3763b..014225692 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/RawDiskSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RawDiskSpecificationFixture.cs
@@ -19,7 +19,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
_remoteEpisode = new RemoteEpisode
{
- Release = new ReleaseInfo() { DownloadProtocol = DownloadProtocol.Torrent }
+ Release = new ReleaseInfo
+ {
+ Title = "Series.title.s01e01",
+ DownloadProtocol = DownloadProtocol.Torrent
+ }
};
}
@@ -29,7 +33,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
}
[Test]
- public void should_return_true_if_no_container_specified()
+ public void should_return_true_if_no_container_specified_and_does_not_match_disc_release_pattern()
{
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
@@ -69,5 +73,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
+ [TestCase("How the Earth Was Made S02 Disc 1 1080i Blu-ray DTS-HD MA 2.0 AVC-TrollHD")]
+ [TestCase("The Universe S03 Disc 1 1080p Blu-ray LPCM 2.0 AVC-TrollHD")]
+ [TestCase("HELL ON WHEELS S02 1080P FULL BLURAY AVC DTS-HD MA 5 1")]
+ [TestCase("Game.of.Thrones.S06.2016.DISC.3.BluRay.1080p.AVC.Atmos.TrueHD7.1-MTeam")]
+ [TestCase("Game of Thrones S05 Disc 1 BluRay 1080p AVC Atmos TrueHD 7 1-MTeam")]
+ public void should_return_false_if_matches_disc_format(string title)
+ {
+ _remoteEpisode.Release.Title = title;
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
+ }
}
}
\ No newline at end of file
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs
index 5ccaaaedb..255920d39 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs
@@ -29,6 +29,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR"
}
};
+
+ Mocker.SetConstant(Mocker.Resolve());
}
private void GivenRestictions(string required, string ignored)
@@ -123,5 +125,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
+
+ [TestCase("/WEB/", true)]
+ [TestCase("/WEB\b/", false)]
+ [TestCase("/WEb/", false)]
+ [TestCase(@"/\.WEB/", true)]
+ public void should_match_perl_regex(string pattern, bool expected)
+ {
+ GivenRestictions(pattern, null);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().Be(expected);
+ }
}
}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs
new file mode 100644
index 000000000..a1e3691a7
--- /dev/null
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Core.Configuration;
+using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
+using NzbDrone.Core.IndexerSearch.Definitions;
+using NzbDrone.Core.MediaFiles;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Profiles;
+using NzbDrone.Core.Qualities;
+using NzbDrone.Core.Tv;
+using NzbDrone.Core.DecisionEngine;
+
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Common.Disk;
+using Moq;
+using NzbDrone.Test.Common;
+using System.IO;
+
+namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
+{
+ [TestFixture]
+ public class DeletedEpisodeFileSpecificationFixture : CoreTest
+ {
+ private RemoteEpisode _parseResultMulti;
+ private RemoteEpisode _parseResultSingle;
+ private EpisodeFile _firstFile;
+ private EpisodeFile _secondFile;
+
+ [SetUp]
+ public void Setup()
+ {
+ _firstFile = new EpisodeFile
+ {
+ Id = 1,
+ RelativePath = "My.Series.S01E01.mkv",
+ Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)),
+ DateAdded = DateTime.Now
+ };
+ _secondFile = new EpisodeFile
+ {
+ Id = 2,
+ RelativePath = "My.Series.S01E02.mkv",
+ Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)),
+ DateAdded = DateTime.Now
+ };
+
+ var singleEpisodeList = new List { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 } };
+ var doubleEpisodeList = new List {
+ new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 },
+ new Episode { EpisodeFile = _secondFile, EpisodeFileId = 2 }
+ };
+
+ var fakeSeries = Builder.CreateNew()
+ .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p })
+ .With(c => c.Path = @"C:\Series\My.Series".AsOsAgnostic())
+ .Build();
+
+ _parseResultMulti = new RemoteEpisode
+ {
+ Series = fakeSeries,
+ ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD, new Revision(version: 2)) },
+ Episodes = doubleEpisodeList
+ };
+
+ _parseResultSingle = new RemoteEpisode
+ {
+ Series = fakeSeries,
+ ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD, new Revision(version: 2)) },
+ Episodes = singleEpisodeList
+ };
+
+ GivenUnmonitorDeletedEpisodes(true);
+ }
+
+ private void GivenUnmonitorDeletedEpisodes(bool enabled)
+ {
+ Mocker.GetMock()
+ .SetupGet(v => v.AutoUnmonitorPreviouslyDownloadedEpisodes)
+ .Returns(enabled);
+ }
+
+ private void WithExistingFile(EpisodeFile episodeFile)
+ {
+ var path = Path.Combine(@"C:\Series\My.Series".AsOsAgnostic(), episodeFile.RelativePath);
+
+ Mocker.GetMock()
+ .Setup(v => v.FileExists(path))
+ .Returns(true);
+ }
+
+ [Test]
+ public void should_return_true_when_unmonitor_deleted_episdes_is_off()
+ {
+ GivenUnmonitorDeletedEpisodes(false);
+
+ Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_when_searching()
+ {
+ Subject.IsSatisfiedBy(_parseResultSingle, new SeasonSearchCriteria()).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_if_file_exists()
+ {
+ WithExistingFile(_firstFile);
+
+ Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_false_if_file_is_missing()
+ {
+ Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_return_true_if_both_of_multiple_episode_exist()
+ {
+ WithExistingFile(_firstFile);
+ WithExistingFile(_secondFile);
+
+ Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_false_if_one_of_multiple_episode_is_missing()
+ {
+ WithExistingFile(_firstFile);
+
+ Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeFalse();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs
new file mode 100644
index 000000000..6dd6f2aec
--- /dev/null
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs
@@ -0,0 +1,110 @@
+using FizzWare.NBuilder;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.DecisionEngine.Specifications;
+using NzbDrone.Core.Indexers;
+using NzbDrone.Core.Indexers.TorrentRss;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Tv;
+using NzbDrone.Test.Common;
+
+namespace NzbDrone.Core.Test.DecisionEngineTests.Search
+{
+ [TestFixture]
+ public class TorrentSeedingSpecificationFixture : TestBase
+ {
+ private Series _series;
+ private RemoteEpisode _remoteEpisode;
+ private IndexerDefinition _indexerDefinition;
+
+ [SetUp]
+ public void Setup()
+ {
+ _series = Builder.CreateNew().With(s => s.Id = 1).Build();
+
+ _remoteEpisode = new RemoteEpisode
+ {
+ Series = _series,
+ Release = new TorrentInfo
+ {
+ IndexerId = 1,
+ Title = "Series.Title.S01.720p.BluRay.X264-RlsGrp",
+ Seeders = 0
+ }
+ };
+
+ _indexerDefinition = new IndexerDefinition
+ {
+ Settings = new TorrentRssIndexerSettings { MinimumSeeders = 5 }
+ };
+
+ Mocker.GetMock()
+ .Setup(v => v.Get(1))
+ .Returns(_indexerDefinition);
+
+ }
+
+ private void GivenReleaseSeeders(int? seeders)
+ {
+ (_remoteEpisode.Release as TorrentInfo).Seeders = seeders;
+ }
+
+ [Test]
+ public void should_return_true_if_not_torrent()
+ {
+ _remoteEpisode.Release = new ReleaseInfo
+ {
+ IndexerId = 1,
+ Title = "Series.Title.S01.720p.BluRay.X264-RlsGrp"
+ };
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_if_indexer_not_specified()
+ {
+ _remoteEpisode.Release.IndexerId = 0;
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_if_indexer_no_longer_exists()
+ {
+ Mocker.GetMock()
+ .Setup(v => v.Get(It.IsAny()))
+ .Callback(i => { throw new ModelNotFoundException(typeof(IndexerDefinition), i); });
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_if_seeds_unknown()
+ {
+ GivenReleaseSeeders(null);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [TestCase(5)]
+ [TestCase(6)]
+ public void should_return_true_if_seeds_above_or_equal_to_limit(int seeders)
+ {
+ GivenReleaseSeeders(seeders);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [TestCase(0)]
+ [TestCase(4)]
+ public void should_return_false_if_seeds_belove_limit(int seeders)
+ {
+ GivenReleaseSeeders(seeders);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs
index d7650c204..0953f6b59 100644
--- a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs
+++ b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs
@@ -130,5 +130,26 @@ namespace NzbDrone.Core.Test.DiskSpace
Mocker.GetMock()
.Verify(v => v.GetAvailableSpace(It.IsAny()), Times.Never());
}
+
+ [TestCase("/boot")]
+ [TestCase("/var/lib/rancher")]
+ [TestCase("/var/lib/rancher/volumes")]
+ [TestCase("/var/lib/kubelet")]
+ [TestCase("/var/lib/docker")]
+ [TestCase("/some/place/docker/aufs")]
+ public void should_not_check_diskspace_for_irrelevant_mounts(string path)
+ {
+ var mount = new Mock();
+ mount.SetupGet(v => v.RootDirectory).Returns(path);
+ mount.SetupGet(v => v.DriveType).Returns(System.IO.DriveType.Fixed);
+
+ Mocker.GetMock()
+ .Setup(v => v.GetMounts())
+ .Returns(new List { mount.Object });
+
+ var freeSpace = Subject.GetFreeSpace();
+
+ freeSpace.Should().BeEmpty();
+ }
}
}
diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs
index 76d22d669..391d50410 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs
@@ -6,7 +6,10 @@ using Moq;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
+using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Pending;
+using NzbDrone.Core.Exceptions;
+using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
@@ -35,7 +38,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
.Build();
}
- private RemoteEpisode GetRemoteEpisode(List episodes, QualityModel quality)
+ private RemoteEpisode GetRemoteEpisode(List episodes, QualityModel quality, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet)
{
var remoteEpisode = new RemoteEpisode();
remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo();
@@ -45,6 +48,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
remoteEpisode.Episodes.AddRange(episodes);
remoteEpisode.Release = new ReleaseInfo();
+ remoteEpisode.Release.DownloadProtocol = downloadProtocol;
remoteEpisode.Release.PublishDate = DateTime.UtcNow;
remoteEpisode.Series = Builder.CreateNew()
@@ -175,7 +179,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
}
[Test]
- public void should_return_an_empty_list_when_none_are_appproved()
+ public void should_return_an_empty_list_when_none_are_approved()
{
var decisions = new List();
decisions.Add(new DownloadDecision(null, new Rejection("Failure!")));
@@ -192,7 +196,6 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List();
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary)));
- decisions.Add(new DownloadDecision(remoteEpisode));
Subject.ProcessDecisions(decisions);
Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Never());
@@ -209,7 +212,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
- Mocker.GetMock().Verify(v => v.Add(It.IsAny()), Times.Never());
+ Mocker.GetMock().Verify(v => v.AddMany(It.IsAny>>()), Times.Never());
}
[Test]
@@ -223,7 +226,64 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
- Mocker.GetMock().Verify(v => v.Add(It.IsAny()), Times.Exactly(2));
+ Mocker.GetMock().Verify(v => v.AddMany(It.IsAny>>()), Times.Once());
+ }
+
+ [Test]
+ public void should_add_to_failed_if_already_failed_for_that_protocol()
+ {
+ var episodes = new List { GetEpisode(1) };
+ var remoteEpisode = GetRemoteEpisode(episodes, new QualityModel(Quality.HDTV720p));
+
+ var decisions = new List();
+ decisions.Add(new DownloadDecision(remoteEpisode));
+ decisions.Add(new DownloadDecision(remoteEpisode));
+
+ Mocker.GetMock().Setup(s => s.DownloadReport(It.IsAny()))
+ .Throws(new DownloadClientUnavailableException("Download client failed"));
+
+ Subject.ProcessDecisions(decisions);
+ Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Once());
+ }
+
+ [Test]
+ public void should_not_add_to_failed_if_failed_for_a_different_protocol()
+ {
+ var episodes = new List { GetEpisode(1) };
+ var remoteEpisode = GetRemoteEpisode(episodes, new QualityModel(Quality.HDTV720p), DownloadProtocol.Usenet);
+ var remoteEpisode2 = GetRemoteEpisode(episodes, new QualityModel(Quality.HDTV720p), DownloadProtocol.Torrent);
+
+ var decisions = new List();
+ decisions.Add(new DownloadDecision(remoteEpisode));
+ decisions.Add(new DownloadDecision(remoteEpisode2));
+
+ Mocker.GetMock().Setup(s => s.DownloadReport(It.Is(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)))
+ .Throws(new DownloadClientUnavailableException("Download client failed"));
+
+ Subject.ProcessDecisions(decisions);
+ Mocker.GetMock().Verify(v => v.DownloadReport(It.Is(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once());
+ Mocker.GetMock().Verify(v => v.DownloadReport(It.Is(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once());
+ }
+
+ [Test]
+ public void should_add_to_rejected_if_release_unavailable_on_indexer()
+ {
+ var episodes = new List { GetEpisode(1) };
+ var remoteEpisode = GetRemoteEpisode(episodes, new QualityModel(Quality.HDTV720p));
+
+ var decisions = new List();
+ decisions.Add(new DownloadDecision(remoteEpisode));
+
+ Mocker.GetMock()
+ .Setup(s => s.DownloadReport(It.IsAny()))
+ .Throws(new ReleaseUnavailableException(remoteEpisode.Release, "That 404 Error is not just a Quirk"));
+
+ var result = Subject.ProcessDecisions(decisions);
+
+ result.Grabbed.Should().BeEmpty();
+ result.Rejected.Should().NotBeEmpty();
+
+ ExceptionVerification.ExpectedWarns(1);
}
}
}
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientStatusServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientStatusServiceFixture.cs
new file mode 100644
index 000000000..08aca1cdb
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientStatusServiceFixture.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Linq;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Core.Download;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Download
+{
+ public class DownloadClientStatusServiceFixture : CoreTest
+ {
+ private DateTime _epoch;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _epoch = DateTime.UtcNow;
+ }
+
+ private DownloadClientStatus WithStatus(DownloadClientStatus status)
+ {
+ Mocker.GetMock()
+ .Setup(v => v.FindByProviderId(1))
+ .Returns(status);
+
+ Mocker.GetMock()
+ .Setup(v => v.All())
+ .Returns(new[] { status });
+
+ return status;
+ }
+
+ private void VerifyUpdate()
+ {
+ Mocker.GetMock()
+ .Verify(v => v.Upsert(It.IsAny()), Times.Once());
+ }
+
+ private void VerifyNoUpdate()
+ {
+ Mocker.GetMock()
+ .Verify(v => v.Upsert(It.IsAny()), Times.Never());
+ }
+
+ [Test]
+ public void should_not_consider_blocked_within_5_minutes_since_initial_failure()
+ {
+ WithStatus(new DownloadClientStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(4),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(4),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+
+ VerifyUpdate();
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().BeNull();
+ }
+
+ [Test]
+ public void should_consider_blocked_after_5_minutes_since_initial_failure()
+ {
+ WithStatus(new DownloadClientStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(6),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+
+ VerifyUpdate();
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().NotBeNull();
+ }
+
+ [Test]
+ public void should_not_escalate_further_till_after_5_minutes_since_initial_failure()
+ {
+ var origStatus = WithStatus(new DownloadClientStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(4),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(4),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().BeNull();
+
+ origStatus.EscalationLevel.Should().Be(3);
+ }
+
+ [Test]
+ public void should_escalate_further_after_5_minutes_since_initial_failure()
+ {
+ WithStatus(new DownloadClientStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(6),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().NotBeNull();
+
+ status.EscalationLevel.Should().BeGreaterThan(3);
+ }
+
+ [Test]
+ public void should_not_escalate_beyond_3_hours()
+ {
+ WithStatus(new DownloadClientStatus
+ {
+ InitialFailure = _epoch - TimeSpan.FromMinutes(6),
+ MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
+ EscalationLevel = 3
+ });
+
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+ Subject.RecordFailure(1);
+
+ var status = Subject.GetBlockedProviders().FirstOrDefault();
+ status.Should().NotBeNull();
+ status.DisabledTill.Should().HaveValue();
+ status.DisabledTill.Should().NotBeAfter(_epoch + TimeSpan.FromHours(3.1));
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs
index 199b206e2..f4fbe8580 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs
@@ -1,15 +1,17 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Download;
-using NzbDrone.Test.Common;
-using System.Threading;
-using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Download.Clients.Blackhole;
+using NzbDrone.Core.MediaFiles;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
{
@@ -18,7 +20,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
{
protected readonly string _title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE";
protected string _completedDownloadFolder = @"c:\blackhole\completed".AsOsAgnostic();
-
+
protected void GivenCompletedItem()
{
var targetDir = Path.Combine(_completedDownloadFolder, _title);
@@ -33,6 +35,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
Mocker.GetMock()
.Setup(c => c.GetFileSize(It.IsAny()))
.Returns(1000000);
+
+ Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>()))
+ .Returns>((b, s) => s.ToList());
}
protected void GivenChangedItem()
@@ -43,7 +48,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
.Setup(c => c.GetFileSize(It.IsAny()))
.Returns(currentSize + 1);
}
-
+
private void VerifySingleItem(DownloadItemStatus status)
{
var items = Subject.GetItems(_completedDownloadFolder, TimeSpan.FromMilliseconds(50)).ToList();
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs
index 5a61271cf..a992c886e 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
@@ -10,6 +11,7 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.Blackhole;
using NzbDrone.Core.Exceptions;
+using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Test.Common;
@@ -48,6 +50,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
Mocker.GetMock()
.Setup(c => c.GetHashFromTorrentFile(It.IsAny()))
.Returns("myhash");
+
+ Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>()))
+ .Returns>((b, s) => s.ToList());
}
protected void GivenFailedDownload()
@@ -99,6 +104,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var result = Subject.GetItems().Single();
VerifyCompleted(result);
+
+ result.CanBeRemoved.Should().BeFalse();
+ result.CanMoveFiles.Should().BeFalse();
}
[Test]
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs
index d48d9e0b8..ff48c1370 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs
@@ -1,5 +1,6 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
@@ -10,6 +11,7 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.Blackhole;
+using NzbDrone.Core.MediaFiles;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
@@ -41,6 +43,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
Mocker.GetMock()
.Setup(c => c.OpenWriteStream(It.IsAny()))
.Returns(() => new FileStream(GetTempFilePath(), FileMode.Create));
+
+ Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>()))
+ .Returns>((b, s) => s.ToList());
}
protected void GivenFailedDownload()
@@ -77,6 +82,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var result = Subject.GetItems().Single();
VerifyCompleted(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs
index af24f2797..f9bbc2ed0 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
protected DelugeTorrent _downloading;
protected DelugeTorrent _failed;
protected DelugeTorrent _completed;
+ protected DelugeTorrent _seeding;
[SetUp]
public void Setup()
@@ -75,7 +76,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
Size = 1000,
BytesDownloaded = 1000,
Progress = 100.0,
- DownloadPath = "somepath"
+ DownloadPath = "somepath",
+ IsAutoManaged = true,
+ StopAtRatio = true,
+ StopRatio = 1.0,
+ Ratio = 1.5
};
Mocker.GetMock()
@@ -114,7 +119,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951".ToLower())
.Callback(PrepareClientToReturnQueuedItem);
}
-
+
protected virtual void GivenTorrents(List torrents)
{
if (torrents == null)
@@ -129,7 +134,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
protected void PrepareClientToReturnQueuedItem()
{
- GivenTorrents(new List
+ GivenTorrents(new List
{
_queued
});
@@ -137,7 +142,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
protected void PrepareClientToReturnDownloadingItem()
{
- GivenTorrents(new List
+ GivenTorrents(new List
{
_downloading
});
@@ -145,7 +150,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
protected void PrepareClientToReturnFailedItem()
{
- GivenTorrents(new List
+ GivenTorrents(new List
{
_failed
});
@@ -189,6 +194,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
VerifyCompleted(item);
+
+ item.CanBeRemoved.Should().BeTrue();
+ item.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -248,11 +256,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
item.Status.Should().Be(expectedItemStatus);
}
- [TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Completed, true)]
- [TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading, true)]
- [TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
- [TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Completed, true)]
- public void GetItems_should_return_completed_item_as_downloadItemStatus(string apiStatus, DownloadItemStatus expectedItemStatus, bool expectedReadOnly)
+ [TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Completed)]
+ [TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading)]
+ [TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Completed)]
+ [TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Completed)]
+ public void GetItems_should_return_completed_item_as_downloadItemStatus(string apiStatus, DownloadItemStatus expectedItemStatus)
{
_completed.State = apiStatus;
@@ -261,24 +269,25 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
- item.IsReadOnly.Should().Be(expectedReadOnly);
}
- [Test]
- public void GetItems_should_check_share_ratio_for_readonly()
+ [TestCase(0.5, false)]
+ [TestCase(1.01, true)]
+ public void GetItems_should_check_share_ratio_for_moveFiles_and_remove(double ratio, bool canBeRemoved)
{
_completed.State = DelugeTorrentStatus.Paused;
_completed.IsAutoManaged = true;
_completed.StopAtRatio = true;
_completed.StopRatio = 1.0;
- _completed.Ratio = 1.01;
+ _completed.Ratio = ratio;
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(DownloadItemStatus.Completed);
- item.IsReadOnly.Should().BeFalse();
+ item.CanMoveFiles.Should().Be(canBeRemoved);
+ item.CanBeRemoved.Should().Be(canBeRemoved);
}
[Test]
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/DownloadStationsTaskStatusJsonConverterFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/DownloadStationsTaskStatusJsonConverterFixture.cs
new file mode 100644
index 000000000..0ad41bbfd
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/DownloadStationsTaskStatusJsonConverterFixture.cs
@@ -0,0 +1,49 @@
+using FluentAssertions;
+using Newtonsoft.Json;
+using NUnit.Framework;
+using NzbDrone.Core.Download.Clients.DownloadStation;
+
+namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
+{
+ [TestFixture]
+ public class DownloadStationsTaskStatusJsonConverterFixture
+ {
+ [TestCase("captcha_needed", DownloadStationTaskStatus.CaptchaNeeded)]
+ [TestCase("filehosting_waiting", DownloadStationTaskStatus.FilehostingWaiting)]
+ [TestCase("hash_checking", DownloadStationTaskStatus.HashChecking)]
+ [TestCase("error", DownloadStationTaskStatus.Error)]
+ [TestCase("downloading", DownloadStationTaskStatus.Downloading)]
+ public void should_parse_enum_correctly(string value, DownloadStationTaskStatus expected)
+ {
+ var task = "{\"Status\": \"" + value + "\"}";
+
+ var item = JsonConvert.DeserializeObject(task);
+
+ item.Status.Should().Be(expected);
+ }
+
+ [TestCase("captcha_needed", DownloadStationTaskStatus.CaptchaNeeded)]
+ [TestCase("filehosting_waiting", DownloadStationTaskStatus.FilehostingWaiting)]
+ [TestCase("hash_checking", DownloadStationTaskStatus.HashChecking)]
+ [TestCase("error", DownloadStationTaskStatus.Error)]
+ [TestCase("downloading", DownloadStationTaskStatus.Downloading)]
+ public void should_serialize_enum_correctly(string expected, DownloadStationTaskStatus value)
+ {
+ var task = new DownloadStationTask { Status = value };
+
+ var item = JsonConvert.SerializeObject(task);
+
+ item.Should().Contain(expected);
+ }
+
+ [Test]
+ public void should_return_unknown_if_unknown_enum_value()
+ {
+ var task = "{\"Status\": \"some_unknown_value\"}";
+
+ var item = JsonConvert.DeserializeObject(task);
+
+ item.Status.Should().Be(DownloadStationTaskStatus.Unknown);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs
index 8269acda6..ed9bc321f 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs
@@ -73,6 +73,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary
{
{ "size_downloaded", "0"},
+ { "size_uploaded", "0"},
{ "speed_download", "0" }
}
}
@@ -96,6 +97,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary
{
{ "size_downloaded", "1000"},
+ { "size_uploaded", "100"},
{ "speed_download", "0" }
},
}
@@ -119,6 +121,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary
{
{ "size_downloaded", "1000"},
+ { "size_uploaded", "100"},
{ "speed_download", "0" }
}
}
@@ -142,6 +145,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary
{
{ "size_downloaded", "100"},
+ { "size_uploaded", "10"},
{ "speed_download", "50" }
}
}
@@ -165,6 +169,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary
{
{ "size_downloaded", "10"},
+ { "size_uploaded", "1"},
{ "speed_download", "0" }
}
}
@@ -188,6 +193,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary
{
{ "size_downloaded", "1000"},
+ { "size_uploaded", "100"},
{ "speed_download", "0" }
}
}
@@ -211,6 +217,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary
{
{ "size_downloaded", "1000"},
+ { "size_uploaded", "100"},
{ "speed_download", "0" }
}
}
@@ -234,6 +241,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary
{
{ "size_downloaded", "1000"},
+ { "size_uploaded", "100"},
{ "speed_download", "0" }
}
}
@@ -257,6 +265,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary
{
{ "size_downloaded", "1000"},
+ { "size_uploaded", "100"},
{ "speed_download", "0" }
}
}
@@ -576,11 +585,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
items.Should().OnlyContain(v => !v.OutputPath.IsEmpty);
}
- [TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading, true)]
- [TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed, false)]
- [TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed, true)]
- [TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued, true)]
- public void GetItems_should_return_readonly_expected(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus, bool readOnlyExpected)
+ [TestCase(DownloadStationTaskStatus.Downloading, false, false)]
+ [TestCase(DownloadStationTaskStatus.Finished, true, true)]
+ [TestCase(DownloadStationTaskStatus.Seeding, true, false)]
+ [TestCase(DownloadStationTaskStatus.Waiting, false, false)]
+ public void GetItems_should_return_canBeMoved_and_canBeDeleted_as_expected(DownloadStationTaskStatus apiStatus, bool canMoveFilesExpected, bool canBeRemovedExpected)
{
GivenSerialNumber();
GivenSharedFolder();
@@ -592,7 +601,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
var items = Subject.GetItems();
items.Should().HaveCount(1);
- items.First().IsReadOnly.Should().Be(readOnlyExpected);
+
+ var item = items.First();
+
+ item.CanBeRemoved.Should().Be(canBeRemovedExpected);
+ item.CanMoveFiles.Should().Be(canMoveFilesExpected);
}
[TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading)]
@@ -601,9 +614,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)]
[TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)]
+ [TestCase(DownloadStationTaskStatus.CaptchaNeeded, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)]
[TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed)]
+ [TestCase(DownloadStationTaskStatus.FilehostingWaiting, DownloadItemStatus.Queued)]
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)]
+ [TestCase(DownloadStationTaskStatus.Unknown, DownloadItemStatus.Queued)]
public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
GivenSerialNumber();
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs
index 48df65841..14c94e808 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs
@@ -408,32 +408,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
items.Should().OnlyContain(v => !v.OutputPath.IsEmpty);
}
- [TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading, true)]
- [TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed, false)]
- [TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued, true)]
- public void GetItems_should_return_readonly_expected(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus, bool readOnlyExpected)
- {
- GivenSerialNumber();
- GivenSharedFolder();
-
- _queued.Status = apiStatus;
-
- GivenTasks(new List() { _queued });
-
- var items = Subject.GetItems();
-
- items.Should().HaveCount(1);
- items.First().IsReadOnly.Should().Be(readOnlyExpected);
- }
-
[TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Error, DownloadItemStatus.Failed)]
[TestCase(DownloadStationTaskStatus.Extracting, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)]
[TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)]
+ [TestCase(DownloadStationTaskStatus.CaptchaNeeded, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)]
+ [TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed)]
+ [TestCase(DownloadStationTaskStatus.FilehostingWaiting, DownloadItemStatus.Queued)]
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)]
+ [TestCase(DownloadStationTaskStatus.Unknown, DownloadItemStatus.Queued)]
public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
GivenSerialNumber();
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs
index adcffe633..5762fdf08 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs
@@ -190,6 +190,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
VerifyCompleted(item);
+
+ item.CanBeRemoved.Should().BeTrue();
+ item.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -298,7 +301,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
.Returns("hash");
var result = Subject.Download(remoteEpisode);
-
+
Assert.IsFalse(result.Any(c => char.IsLower(c)));
}
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs
index ccdaba3f1..c18bb7b94 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Collections.Generic;
using FluentAssertions;
@@ -103,10 +103,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
public void queued_item_should_have_required_properties()
{
GivenQueue(_queued);
-
+
var result = Subject.GetItems().Single();
VerifyQueued(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -118,6 +121,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
var result = Subject.GetItems().Single();
VerifyPaused(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -129,6 +135,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
var result = Subject.GetItems().Single();
VerifyDownloading(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -139,6 +148,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
var result = Subject.GetItems().Single();
VerifyCompleted(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -149,6 +161,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
var result = Subject.GetItems().Single();
VerifyFailed(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs
index 226288464..feea1329d 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Collections.Generic;
using FluentAssertions;
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
private NzbgetQueueItem _queued;
private NzbgetHistoryItem _failed;
private NzbgetHistoryItem _completed;
+ private Dictionary _configItems;
[SetUp]
public void Setup()
@@ -80,13 +81,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
DownloadRate = 7000000
});
- var configItems = new Dictionary();
- configItems.Add("Category1.Name", "tv");
- configItems.Add("Category1.DestDir", @"/remote/mount/tv");
+
+ Mocker.GetMock()
+ .Setup(v => v.GetVersion(It.IsAny()))
+ .Returns("14.0");
+
+ _configItems = new Dictionary();
+ _configItems.Add("Category1.Name", "tv");
+ _configItems.Add("Category1.DestDir", @"/remote/mount/tv");
Mocker.GetMock()
.Setup(v => v.GetConfig(It.IsAny()))
- .Returns(configItems);
+ .Returns(_configItems);
}
protected void GivenFailedDownload()
@@ -163,10 +169,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
GivenQueue(_queued);
GivenHistory(null);
-
+
var result = Subject.GetItems().Single();
VerifyQueued(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -180,6 +189,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
var result = Subject.GetItems().Single();
VerifyPaused(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -193,6 +205,25 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
var result = Subject.GetItems().Single();
VerifyDownloading(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
+ }
+
+ [Test]
+ public void post_processing_item_should_have_required_properties()
+ {
+ _queued.ActiveDownloads = 1;
+
+ GivenQueue(_queued);
+ GivenHistory(null);
+
+ _queued.RemainingSizeLo = 0;
+
+ var result = Subject.GetItems().Single();
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -204,6 +235,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
var result = Subject.GetItems().Single();
VerifyCompleted(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -386,5 +420,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
error.IsValid.Should().Be(expected);
}
+
+ [TestCase("0", false)]
+ [TestCase("1", true)]
+ [TestCase(" 7", false)]
+ [TestCase("5000000", false)]
+ public void should_test_keephistory(string keephistory, bool expected)
+ {
+ _configItems["KeepHistory"] = keephistory;
+
+ var error = Subject.Test();
+
+ error.IsValid.Should().Be(expected);
+ }
}
}
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs
index 3ceece6f6..d7146acbe 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs
@@ -89,6 +89,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
});
}
+ protected void GivenHighPriority()
+ {
+ Subject.Definition.Settings.As().OlderTvPriority = (int)QBittorrentPriority.First;
+ Subject.Definition.Settings.As().RecentTvPriority = (int)QBittorrentPriority.First;
+ }
+
protected void GivenMaxRatio(float maxRatio, bool removeOnMaxRatio = true)
{
Mocker.GetMock()
@@ -265,6 +271,39 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
id.Should().Be(expectedHash);
}
+ [Test]
+ public void Download_should_set_top_priority()
+ {
+ GivenHighPriority();
+ GivenSuccessfulDownload();
+
+ var remoteEpisode = CreateRemoteEpisode();
+
+ var id = Subject.Download(remoteEpisode);
+
+ Mocker.GetMock()
+ .Verify(v => v.MoveTorrentToTopInQueue(It.IsAny(), It.IsAny()), Times.Once());
+ }
+
+ [Test]
+ public void Download_should_not_fail_if_top_priority_not_available()
+ {
+ GivenHighPriority();
+ GivenSuccessfulDownload();
+
+ Mocker.GetMock()
+ .Setup(v => v.MoveTorrentToTopInQueue(It.IsAny(), It.IsAny()))
+ .Throws(new HttpException(new HttpResponse(new HttpRequest("http://me.local/"), new HttpHeader(), new byte[0], System.Net.HttpStatusCode.Forbidden)));
+
+ var remoteEpisode = CreateRemoteEpisode();
+
+ var id = Subject.Download(remoteEpisode);
+
+ id.Should().NotBeNullOrEmpty();
+
+ ExceptionVerification.ExpectedWarns(1);
+ }
+
[Test]
public void should_return_status_with_outputdirs()
{
@@ -311,7 +350,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
}
[Test]
- public void should_be_read_only_if_max_ratio_not_reached()
+ public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_not_reached()
{
GivenMaxRatio(1.0f);
@@ -330,11 +369,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
GivenTorrents(new List { torrent });
var item = Subject.GetItems().Single();
- item.IsReadOnly.Should().BeTrue();
+ item.CanBeRemoved.Should().BeFalse();
+ item.CanMoveFiles.Should().BeFalse();
}
[Test]
- public void should_be_read_only_if_max_ratio_reached_and_not_paused()
+ public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_paused()
{
GivenMaxRatio(1.0f);
@@ -353,11 +393,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
GivenTorrents(new List { torrent });
var item = Subject.GetItems().Single();
- item.IsReadOnly.Should().BeTrue();
+ item.CanBeRemoved.Should().BeFalse();
+ item.CanMoveFiles.Should().BeFalse();
}
[Test]
- public void should_be_read_only_if_max_ratio_is_not_set()
+ public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
{
GivenMaxRatio(1.0f, false);
@@ -376,11 +417,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
GivenTorrents(new List { torrent });
var item = Subject.GetItems().Single();
- item.IsReadOnly.Should().BeTrue();
+ item.CanBeRemoved.Should().BeFalse();
+ item.CanMoveFiles.Should().BeFalse();
}
[Test]
- public void should_not_be_read_only_if_max_ratio_reached_and_paused()
+ public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
{
GivenMaxRatio(1.0f);
@@ -399,7 +441,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
GivenTorrents(new List { torrent });
var item = Subject.GetItems().Single();
- item.IsReadOnly.Should().BeFalse();
+ item.CanBeRemoved.Should().BeTrue();
+ item.CanMoveFiles.Should().BeTrue();
}
[Test]
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs
index e58e4b9a8..b3c1dc680 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Collections.Generic;
using FizzWare.NBuilder;
@@ -191,7 +191,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
var result = Subject.GetItems().Single();
VerifyQueued(result);
+
result.RemainingTime.Should().NotBe(TimeSpan.Zero);
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[TestCase(SabnzbdDownloadStatus.Paused)]
@@ -205,6 +208,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
var result = Subject.GetItems().Single();
VerifyPaused(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[TestCase(SabnzbdDownloadStatus.Checking)]
@@ -227,7 +233,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
var result = Subject.GetItems().Single();
VerifyDownloading(result);
+
result.RemainingTime.Should().NotBe(TimeSpan.Zero);
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -239,6 +248,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
var result = Subject.GetItems().Single();
VerifyCompleted(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -252,6 +264,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
var result = Subject.GetItems().Single();
VerifyFailed(result);
+
+ result.CanBeRemoved.Should().BeTrue();
+ result.CanMoveFiles.Should().BeTrue();
}
[Test]
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs
index 39ec56789..032fe15ff 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs
@@ -41,6 +41,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
VerifyCompleted(item);
+
+ item.CanBeRemoved.Should().BeTrue();
+ item.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -145,8 +148,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
[TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading)]
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
- [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed)]
- [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)]
+ [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Downloading)]
+ [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Downloading)]
public void GetItems_should_return_queued_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
_queued.Status = apiStatus;
@@ -160,7 +163,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
- [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)]
+ [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Downloading)]
public void GetItems_should_return_downloading_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
_downloading.Status = apiStatus;
@@ -172,13 +175,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
item.Status.Should().Be(expectedItemStatus);
}
- [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, false)]
- [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, true)]
- [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, true)]
- [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
- [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, true)]
- [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, true)]
- public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedReadOnly)
+ [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, true)]
+ [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, false)]
+ [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, false)]
+ [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Completed, false)]
+ [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, false)]
+ [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, false)]
+ public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedValue)
{
_completed.Status = apiStatus;
@@ -187,7 +190,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
- item.IsReadOnly.Should().Be(expectedReadOnly);
+ item.CanBeRemoved.Should().Be(expectedValue);
+ item.CanMoveFiles.Should().Be(expectedValue);
}
[Test]
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs
index 1d9f037d2..e66ce42dc 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs
@@ -222,6 +222,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
VerifyCompleted(item);
+
+ item.CanBeRemoved.Should().BeTrue();
+ item.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -292,12 +295,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
item.Status.Should().Be(expectedItemStatus);
}
- [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued, false)]
- [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked, DownloadItemStatus.Completed, false)]
- [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
- [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Started, DownloadItemStatus.Completed, true)]
- [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Paused, DownloadItemStatus.Completed, true)]
- public void GetItems_should_return_completed_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedReadOnly)
+ [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued, true)]
+ [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked, DownloadItemStatus.Completed, true)]
+ [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued, DownloadItemStatus.Completed, false)]
+ [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Started, DownloadItemStatus.Completed, false)]
+ [TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Paused, DownloadItemStatus.Completed, false)]
+ public void GetItems_should_return_completed_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedValue)
{
_completed.Status = apiStatus;
@@ -306,7 +309,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
- item.IsReadOnly.Should().Be(expectedReadOnly);
+ item.CanBeRemoved.Should().Be(expectedValue);
+ item.CanMoveFiles.Should().Be(expectedValue);
}
[Test]
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs
index 00278c811..406e70e15 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs
@@ -13,6 +13,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
[TestFixture]
public class VuzeFixture : TransmissionFixtureBase
{
+ [SetUp]
+ public void Setup_Vuze()
+ {
+ // Vuze never sets isFinished.
+ _completed.IsFinished = false;
+ }
+
[Test]
public void queued_item_should_have_required_properties()
{
@@ -43,6 +50,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
VerifyCompleted(item);
+
+ item.CanBeRemoved.Should().BeTrue();
+ item.CanMoveFiles.Should().BeTrue();
}
[Test]
@@ -147,8 +157,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
[TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading)]
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
- [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed)]
- [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)]
+ [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Downloading)]
+ [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Downloading)]
public void GetItems_should_return_queued_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
_queued.Status = apiStatus;
@@ -162,7 +172,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
- [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)]
+ [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Downloading)]
public void GetItems_should_return_downloading_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
_downloading.Status = apiStatus;
@@ -174,13 +184,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
item.Status.Should().Be(expectedItemStatus);
}
- [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, false)]
- [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, true)]
- [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, true)]
- [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
- [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, true)]
- [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, true)]
- public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedReadOnly)
+ [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, true)]
+ [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, false)]
+ [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, false)]
+ [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued, false)]
+ [TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, false)]
+ [TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, false)]
+ public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedValue)
{
_completed.Status = apiStatus;
@@ -189,7 +199,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
- item.IsReadOnly.Should().Be(expectedReadOnly);
+ item.CanBeRemoved.Should().Be(expectedValue);
+ item.CanMoveFiles.Should().Be(expectedValue);
}
[Test]
@@ -294,7 +305,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
}
[Test]
- public void should_have_correct_output_directory()
+ public void should_have_correct_output_directory_for_multifile_torrents()
{
WindowsOnly();
@@ -311,5 +322,25 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
items.First().OutputPath.Should().Be(@"C:\Downloads\" + _title);
}
+ [Test]
+ public void should_have_correct_output_directory_for_singlefile_torrents()
+ {
+ WindowsOnly();
+
+ var fileName = _title + ".mkv";
+ _downloading.Name = fileName;
+ _downloading.DownloadDir = @"C:/Downloads";
+
+ GivenTorrents(new List
+ {
+ _downloading
+ });
+
+ var items = Subject.GetItems().ToList();
+
+ items.Should().HaveCount(1);
+ items.First().OutputPath.Should().Be(@"C:\Downloads\" + fileName);
+ }
+
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs
index b82216b19..1b49ef811 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs
@@ -22,6 +22,7 @@ namespace NzbDrone.Core.Test.Download
{
private RemoteEpisode _parseResult;
private List _downloadClients;
+
[SetUp]
public void Setup()
{
@@ -82,7 +83,7 @@ namespace NzbDrone.Core.Test.Download
{
var mock = WithUsenetClient();
mock.Setup(s => s.Download(It.IsAny()));
-
+
Subject.DownloadReport(_parseResult);
VerifyEventPublished();
@@ -93,7 +94,7 @@ namespace NzbDrone.Core.Test.Download
{
var mock = WithUsenetClient();
mock.Setup(s => s.Download(It.IsAny()));
-
+
Subject.DownloadReport(_parseResult);
mock.Verify(s => s.Download(It.IsAny()), Times.Once());
@@ -117,7 +118,7 @@ namespace NzbDrone.Core.Test.Download
var mock = WithUsenetClient();
mock.Setup(s => s.Download(It.IsAny()))
.Callback(v => {
- throw new ReleaseDownloadException(v.Release, "Error", new WebException());
+ throw new ReleaseDownloadException(v.Release, "Error", new WebException());
});
Assert.Throws(() => Subject.DownloadReport(_parseResult));
@@ -136,7 +137,7 @@ namespace NzbDrone.Core.Test.Download
var mock = WithUsenetClient();
mock.Setup(s => s.Download(It.IsAny()))
.Callback(v => {
- throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
+ throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
});
Assert.Throws(() => Subject.DownloadReport(_parseResult));
@@ -180,14 +181,50 @@ namespace NzbDrone.Core.Test.Download
}
[Test]
- public void should_not_attempt_download_if_client_isnt_configure()
+ public void Download_report_should_not_trigger_indexer_backoff_on_indexer_404_error()
{
- Subject.DownloadReport(_parseResult);
+ var mock = WithUsenetClient();
+ mock.Setup(s => s.Download(It.IsAny()))
+ .Callback(v => {
+ throw new ReleaseUnavailableException(v.Release, "Error", new WebException());
+ });
+
+ Assert.Throws(() => Subject.DownloadReport(_parseResult));
+
+ Mocker.GetMock()
+ .Verify(v => v.RecordFailure(It.IsAny(), It.IsAny()), Times.Never());
+ }
+
+ [Test]
+ public void should_not_attempt_download_if_client_isnt_configured()
+ {
+ Assert.Throws(() => Subject.DownloadReport(_parseResult));
Mocker.GetMock().Verify(c => c.Download(It.IsAny()), Times.Never());
VerifyEventNotPublished();
+ }
- ExceptionVerification.ExpectedWarns(1);
+ [Test]
+ public void should_attempt_download_even_if_client_is_disabled()
+ {
+ var mockUsenet = WithUsenetClient();
+
+ Mocker.GetMock()
+ .Setup(v => v.GetBlockedProviders())
+ .Returns(new List
+ {
+ new DownloadClientStatus
+ {
+ ProviderId = _downloadClients.First().Definition.Id,
+ DisabledTill = DateTime.UtcNow.AddHours(3)
+ }
+ });
+
+ Subject.DownloadReport(_parseResult);
+
+ Mocker.GetMock().Verify(c => c.GetBlockedProviders(), Times.Never());
+ mockUsenet.Verify(c => c.Download(It.IsAny()), Times.Once());
+ VerifyEventPublished();
}
[Test]
diff --git a/src/NzbDrone.Core.Test/Download/NzbValidationServiceFixture.cs b/src/NzbDrone.Core.Test/Download/NzbValidationServiceFixture.cs
new file mode 100644
index 000000000..557a28ae0
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Download/NzbValidationServiceFixture.cs
@@ -0,0 +1,43 @@
+using System.IO;
+using NUnit.Framework;
+using NzbDrone.Core.Download;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Download
+{
+ [TestFixture]
+ public class NzbValidationServiceFixture : CoreTest
+ {
+ private byte[] GivenNzbFile(string name)
+ {
+ return File.ReadAllBytes(GetTestPath("Files/Nzbs/" + name + ".nzb"));
+ }
+
+ [Test]
+ public void should_throw_on_invalid_nzb()
+ {
+ var filename = "NotNzb";
+ var fileContent = GivenNzbFile(filename);
+
+ Assert.Throws(() => Subject.Validate(filename, fileContent));
+ }
+
+ [Test]
+ public void should_throw_when_no_files()
+ {
+ var filename = "NoFiles";
+ var fileContent = GivenNzbFile(filename);
+
+ Assert.Throws(() => Subject.Validate(filename, fileContent));
+ }
+
+ [Test]
+ public void should_validate_nzb()
+ {
+ var filename = "ValidNzb";
+ var fileContent = GivenNzbFile(filename);
+
+ Subject.Validate(filename, fileContent);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs
index 2a5a29c6b..5820b1f0b 100644
--- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using FizzWare.NBuilder;
using Marr.Data;
using Moq;
@@ -26,6 +27,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
private ReleaseInfo _release;
private ParsedEpisodeInfo _parsedEpisodeInfo;
private RemoteEpisode _remoteEpisode;
+ private List _heldReleases;
[SetUp]
public void Setup()
@@ -60,17 +62,27 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
_remoteEpisode.Series = _series;
_remoteEpisode.ParsedEpisodeInfo = _parsedEpisodeInfo;
_remoteEpisode.Release = _release;
-
+
_temporarilyRejected = new DownloadDecision(_remoteEpisode, new Rejection("Temp Rejected", RejectionType.Temporary));
+ _heldReleases = new List();
+
Mocker.GetMock()
.Setup(s => s.All())
- .Returns(new List());
+ .Returns(_heldReleases);
+
+ Mocker.GetMock()
+ .Setup(s => s.AllBySeriesId(It.IsAny()))
+ .Returns(i => _heldReleases.Where(v => v.SeriesId == i).ToList());
Mocker.GetMock()
.Setup(s => s.GetSeries(It.IsAny()))
.Returns(_series);
+ Mocker.GetMock()
+ .Setup(s => s.GetSeries(It.IsAny>()))
+ .Returns(new List { _series });
+
Mocker.GetMock()
.Setup(s => s.GetEpisodes(It.IsAny