1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

..

223 Commits

Author SHA1 Message Date
Taloth Saldono 779ab39f50 Fixed failing test 2019-01-12 13:30:08 +01:00
Taloth Saldono 00283e3d6e New: Limit indexer/download client backoff to 5 min during the first 15 min of application start.
closes #2366
2019-01-12 13:15:41 +01:00
Taloth Saldono 2b4429f8b7 Fixed: Erroneously matching Anime 10.5 special as 10.
fixes #2868
2019-01-12 13:14:47 +01:00
Taloth Saldono 2446c4185a Added 10-bit to parser cleanup.
fixes #2870
2019-01-12 13:14:47 +01:00
Taloth Saldono 04900e5f90 Tweaked reverse title detection to handle triple digit episode numbers.
fixes #2871
2019-01-12 13:14:47 +01:00
Taloth Saldono ce59db528b Fixed: Mono bug causing memory leakage when http connections use gzip compression.
The bug is registered upstream, but this commit works around the problem by doing the gzip decompression separately from the http stack.

Ref #2296
2019-01-10 20:13:48 +01:00
Taloth Saldono 31b266659e Fixed bad test due to skyhook now doing it's own fuzzy search. 2018-12-29 13:05:03 +01:00
Taloth Saldono e071b0c2e0 DataMapper LazyLoaded needlessly keeping the parent mapper alive. 2018-12-29 12:45:07 +01:00
Taloth Saldono 270f04d2d2 Fixed: Excessive memory usage due to sqlite cache configuration.
ref #2296
2018-12-29 12:43:35 +01:00
Mark McDowall 9af57c6786 New: Store last search time for EpisodeSearch
Closes #420
2018-12-06 20:59:09 -08:00
Mark McDowall ff4a550cbb New: Include OriginalFilePath with Episode Files
Closes #2336
2018-12-06 20:59:09 -08:00
Kevin Richter 537e4d7c39 Fix Quality Detection with DDP5.1 2018-11-24 11:24:24 +01:00
Taloth Saldono 9f16d9b2fc Fixed: File names and release titles lacking a series title and starting with the Air date.
fixes #2825
2018-11-21 22:02:51 +01:00
Taloth Saldono ae6d920e2a Updated error message if skyhook and other services respond with html content.
closes #2817
2018-11-14 21:48:56 +01:00
Mark McDowall 0d22f9ec29 Improve logging when rejecting release with unmonitored episodes 2018-11-11 21:11:14 -08:00
Mark McDowall 699076a405 New: Added warning for Download Station that 2FA is not supported
Closes #2451
2018-11-10 16:23:33 -08:00
Jeffrey Neer df593f486f New: Added priority levels to Join Notifications 2018-11-10 14:51:14 -08:00
Mark McDowall 0d95873a05 New: Parsing french anime releases with single absolute episode number
Closes #2798
2018-11-03 18:42:06 -07:00
Mark McDowall b20acc9063 Fixed: Sort The A-Team properly in series list 2018-11-03 11:58:00 -07:00
Mark McDowall 70d6d25178 New: Parse names with 1080i as 1080p if they are not RAW HD
Closes #2793
2018-11-03 11:52:02 -07:00
Mark McDowall 196d165432 New: Parse names with FHD as 1080p
Closes #2793
2018-11-03 11:45:34 -07:00
Mark McDowall bb3ca998fc Restrict 4k parsing to avoid false positives 2018-11-03 11:30:41 -07:00
Mark McDowall da73221cef Fixed: Handling of poorly formed items when parsing results from indexer 2018-10-24 20:43:52 -07:00
Mark McDowall 36f66eed21 New: Parse names with 4k as 2160p
Closes #2788
2018-10-24 20:13:57 -07:00
Mark McDowall 8e916d60f5 Fixed: Parsing of specials with only season and episode numbers in the file name 2018-10-24 18:32:22 -07:00
Mark McDowall 44048207f2 Remove file quality matches release import spec
New: Don't reject imports when quality doesn't match release quality
New: Reject grab when release was grabbed and imported already
Closes #2783
2018-10-22 20:37:32 -07:00
Mark McDowall b73b99df8d Fixed: Don't clean Kodi library if Always Update is disabled and video is playing
Fixes #2773
2018-10-22 14:20:22 -07:00
Mark McDowall ad69ecc5eb Fixed: Use season number from episode instead of parsed from release for custom scripts
Closes #2748
2018-10-07 19:03:32 -07:00
Mark McDowall 1304bc8fb9 Fixed: Exclude /snap/* locations from disk space
Closes #2743
2018-10-07 19:03:32 -07:00
Mark McDowall a4f63e728c Fixed: Don't use media info for non-video files
Fixes #2745
2018-10-07 19:03:32 -07:00
Jeff Byrnes 307b3536b7 New: Compatibility with Hombrew-installed mono 2018-09-15 10:49:22 -07:00
Mark McDowall 24c6d3f4b3 Don't read response stream if it equals Stream.Null 2018-09-14 17:50:13 -07:00
Mark McDowall 4a052708c8 New: Updated pushover app clone URL 2018-09-04 00:19:09 -07:00
Mark McDowall 39a8d4f0d8 Fixed: Parsing of new hashed release filenames (######_##.ext) 2018-09-03 11:24:48 -07:00
Mark McDowall ca22a25842 New: Add stopped option for rTorrent 2018-08-28 18:14:55 -07:00
Mark McDowall ff9a9a5e4d More restrictions when using download client title or folder name for parsing
Fixes #2663
2018-08-27 21:35:03 -07:00
Mark McDowall 3d7c59bc3b New: Add unique IDs to Kodi metadata
Closes #2711
2018-08-27 20:42:32 -07:00
Mark McDowall 63ea1f1afd Fixed: Skip sample check when rescanning series folder 2018-08-19 09:23:34 -07:00
Mark McDowall baf8f6cca6 Fixed: Parsing multi-episode in square bracket
Fixes #2669
2018-08-18 11:22:51 -07:00
Mark McDowall c67c7e1b5a More flexible matching some anime releases 2018-08-18 10:56:18 -07:00
Mark McDowall 46d8e5830a Fixed: Concurrent manual imports silently failing 2018-08-18 10:56:18 -07:00
Taloth Saldono 37054673b7 Fixed: Too big eta in qbit api still occurring on official builds. 2018-08-07 19:08:38 +02:00
Mark McDowall 86bc5c5547 Fixed: Parsing of some anime releases with season number in title
Fixes #2684
2018-07-30 19:20:40 -07:00
Taloth Saldono fc44607c73 Added missing UrlBase validation for SabnzbdSettings. 2018-07-18 07:39:58 +02:00
Taloth Saldono 2a1421f488 Fixed: Skip torrents in Deluge api that don't have hashes.
closes #2566
closes #2567
closes #2664
2018-07-16 19:16:26 +02:00
Nicholas Landriault d7a054f637 Deluge torrents that don't have a hash are skipped
In some cases torrents in Deluge may not have a hash (ex: https://torguard.net/checkmytorrentipaddress.php). This causes Sonarr to fail when loading the torrents from Deluge with error message: 'Unable to communicate with deluge. Object reference not set to an instance of an object'. This commit simply causes Sonarr to skip over the torrent with the missing hash and continue loading torrents that do have hashes.
2018-07-16 19:11:10 +02:00
Taloth Saldono 9c9ad9aec3 New: Added optional UrlBase to Nzbget and Sabnzbd settings.
ref #1651
2018-07-15 12:24:27 +02:00
Mark McDowall 1467c52e03 Fixed: Multi-file torrents in Vuze with different folder and file names
Fixes #2571
2018-07-11 19:01:41 -07:00
Mark McDowall e407145d10 Fixed: .vtt files treated as subtitles 2018-07-08 19:22:42 -07:00
Taloth Saldono 476110b1de Fixed: Store BitRate_Nominal (VBR) mediainfo in database instead of only BitRate.
ref Radarr/Radarr#2860
2018-07-08 18:36:36 +02:00
Taloth Saldono 45f9f45f50 Fixed: Quality parser for the rare HD-DVD. 2018-07-07 20:04:45 +02:00
Taloth Saldono d581d997c2 Fixed: Ignore /etc in System disk overview. 2018-07-07 11:09:04 +02:00
Taloth Saldono 633344e5bb Disabled httpbin.org tests for now due to the site being flaky. 2018-07-06 21:53:03 +02:00
Taloth Saldono 0cce6b74f9 Added logging of json snippets on json deserialization errors. 2018-07-06 21:44:15 +02:00
Taloth Saldono 8b8bfb9bf0 Added third httpbin site. 2018-07-06 19:50:29 +02:00
Taloth Saldono 7241ca4ae9 Run http tests more gracefully. 2018-07-06 19:09:59 +02:00
Taloth Saldono e9b11e55e9 Fixed: Regression with importing nested obfuscated directories.
Closes #2640
2018-07-04 21:55:08 +02:00
Mark McDowall 48126f55ed Fixed: Parsing titles with question marks
Fixes #2637
2018-06-27 19:54:25 -07:00
Mark McDowall cb549507ee Fixed: Parsing dates using underscores for separation 2018-06-27 18:32:28 -07:00
Mark McDowall a0b6cdb08e Fixed: Forced seeding in QBittorrent status treated as complete 2018-06-27 18:31:44 -07:00
Taloth Saldono 9b9597093c Fixed: Regression causing Manual Import to ignore user provided information. 2018-06-21 07:23:20 +02:00
Taloth Saldono c92fb4d9c0 Fixed: Mini-series with multiple episode E01-E02.
fixes #2614
2018-06-17 15:57:33 +02:00
Taloth Saldono 88c659059b Added {MediaInfo AudioLanguages} {.. SubtitleLanguages}
fixes #2593
2018-06-16 22:19:33 +02:00
Taloth Saldono d1ced600a1 Handle empty Episode list during decision maker. 2018-06-16 21:44:37 +02:00
Taloth Saldono a5977f2752 Fixed: Manual Import not using Release Group during renamer. [develop only]
fixes #2612
2018-06-16 21:44:37 +02:00
Mark McDowall 8668e8b036 Use FolderEpisodeInfo instead of parent of FileEpisodeInfo
Fixes #2338
Fixed: Detecting some incorrect file name paring during import
2018-06-09 21:42:01 -07:00
Mark McDowall 783c27a584 Fixed: Initially pausing torrents in QBittorrent
Fixes #2599
2018-06-08 16:38:20 -07:00
Taloth Saldono fde0409650 Fixed WithData sample length not using parameter. 2018-06-08 22:08:09 +02:00
Taloth Saldono 2ed5abf4d3 Also add as data to exception so sentry gets it. 2018-06-08 19:58:58 +02:00
Taloth Saldono cb372f284d Log indexer response to Trace if an exception occurs. 2018-06-08 19:29:08 +02:00
Taloth Saldono 5d674a07f7 Fixed SeedConfigProvider failing on ReleasePush. 2018-05-26 00:16:34 +02:00
Mark McDowall 59e69c1839 Update bug issue template 2018-05-21 12:43:07 -07:00
Mark McDowall 89a3eec6ae Fixed: Parsing of anime releases with year and EP before episode number
Fixes #2578
2018-05-21 00:31:03 -07:00
Mark McDowall 6517f1af14 New: Treat 1440p releases as 1080p instead of 480p
Closes #2558
2018-05-20 23:33:45 -07:00
Taloth Saldono b95c4d37d7 Fixed validation error for Seed Ratio on btn. 2018-05-20 12:34:24 +02:00
Taloth Saldono 7388e8c2c2 Fixed broken test for nested settings. 2018-05-20 10:19:52 +02:00
Taloth Saldono b837ab41eb Fixed broken tests. 2018-05-19 22:15:19 +02:00
Taloth Saldono de6615f586 Added a few more units. 2018-05-19 21:07:01 +02:00
Kevin Richter 0947dfc423 Stop deluge torrent when they reach stop ratio 2018-05-19 21:07:01 +02:00
Kevin Richter 002ed9e4c7 Fix parsing of entered time for seed time 2018-05-19 21:07:01 +02:00
Taloth Saldono 1a6a3038d6 Added warnings for minimum criteria for BTN. 2018-05-19 21:07:01 +02:00
Taloth Saldono d86beb06f7 Added fancy unit indicator to fields. 2018-05-19 21:07:01 +02:00
Taloth Saldono 6df61e305d Added Seed Time and Season-Pack seed time. 2018-05-19 21:07:00 +02:00
Taloth Saldono 47018b02a8 Added nested settings for seed criteria. 2018-05-19 21:07:00 +02:00
Taloth Saldono b6ef4d50dc Fixed more C#7. 2018-05-19 21:07:00 +02:00
Kevin Richter 2d86e44c63 New: Added advanced setting per indexer to override seed ratio limit for supported clients. 2018-05-19 21:07:00 +02:00
Taloth Saldono 69f8fc4d5e Added support for nested settings models so settings can be grouped together and reused for multiple providers. 2018-05-19 21:06:59 +02:00
Taloth Saldono b339fcbd82 Fixed mono debug check. 2018-05-19 21:06:08 +02:00
Mark McDowall 7d1b09ac1b Update issue templates 2018-05-18 14:40:18 -07:00
Mark McDowall 55d01f620a Fixed: Setting inital state of torrents sent to QBittorrent
Fixes #2565
2018-05-16 14:00:21 -07:00
Mark McDowall 39d0d08ced Fixed: Removed old warning that Torrent Blackhole does not support magnet links 2018-05-16 13:59:33 -07:00
Mark McDowall 94f8cc9b62 Improve parsing of standard titles with junk in []
Fixed: Improve parsing of standard series releases/files with square brackets at the end
2018-05-11 19:08:47 -07:00
Mark McDowall 17b998f01f Improve parsing of non-standard date releases
Fixed: Parsing of some releases with season and episode numbers along with date
2018-05-11 18:34:34 -07:00
Mark McDowall 18415ddb30 New: Remove additional URL prefixes from release names
Closes #2542
2018-05-09 13:53:37 -07:00
Mark McDowall 2b3d0235cf Don't read media info when disabled in settings
Fixed: Don't read media info for existing files if "Analyse video files" disabled
Fixes #2549
2018-05-09 13:53:27 -07:00
Mark McDowall c687f45ff6 Fixed: Don't try to find episodes if parsing failed 2018-05-09 13:35:56 -07:00
Mark McDowall 13f540f1f5 Fixed: Rescan series if refresh fails
Closes #2540
2018-05-02 23:34:05 -07:00
Mark McDowall a7aff3bd9a Upgrade NLog to 4.5.3
Closes #2535
2018-05-01 23:20:54 -07:00
Taloth Saldono 03997e2a0d Switched to BigInteger for qbit eta as workaround for api bug, tyvm. 2018-04-30 22:18:42 +02:00
Mark McDowall 60c73df685 Fixed: Custom script unable to execute when release processed via /release/push API 2018-04-29 22:04:18 -07:00
Mark McDowall b5ce3b2773 Fixed: Improved parsing of iTunes named files 2018-04-29 21:56:33 -07:00
Qstick 6c09b7dc28 Fixed: Throw SonarrStartupException if can't access AppFolder Location 2018-04-22 13:25:50 -07:00
Mark McDowall bf1fbd7fc4 Fixed: Remove leading space from file names
Fixes #2365
2018-04-22 12:40:11 -07:00
Mark McDowall 401530db65 Fixed RemoveGrabbed tests 2018-04-22 09:40:44 -07:00
Mark McDowall 4fb7cb3a8b Fixed RemoveRejected tests 2018-04-22 09:27:07 -07:00
Mark McDowall aba8abb176 Fixed: Suppress warnings for daily style extra files
Fixes #2503
2018-04-21 15:25:21 -07:00
Taloth Saldono 2b3956cb60 Honor x264 tag in filename if mediainfo does not conclusively indicate h264.
Closes #2500
2018-04-18 22:31:56 +02:00
Taloth Saldono 1b0647423d Updated a few harder to detect MediaInfo VideoCodec formats. 2018-04-18 22:07:38 +02:00
Taloth Saldono 5b627a8bc2 Fixed missing existingfile flag. 2018-04-15 16:52:30 +02:00
Taloth Saldono 62f7e90748 Fixed db migration issue. 2018-04-14 22:24:48 +02:00
Taloth Saldono fd1064cb69 Fixed errors in MatchesFolderSpecification and tests. 2018-04-14 22:21:48 +02:00
Taloth Saldono c677736a8f Fixed: Hide fallback pending releases if temporarily delayed.
Also batch changing of Pending Release Reason.
2018-04-14 22:07:08 +02:00
Taloth Saldono 67038ddd5e Cache EventAggregator Subscribers. 2018-04-14 22:07:08 +02:00
Taloth Saldono ff8eb0b67f Added additional indexes to speed up DecisionMaker performance. 2018-04-14 22:07:08 +02:00
Taloth Saldono 70afacee3f Refactored PendingRelease logic for performance. 2018-04-14 22:07:08 +02:00
Taloth Saldono 5b0e959d3f Debounce Command Notifications. 2018-04-14 22:07:08 +02:00
Taloth Saldono b7c5639a9d Speed up sqlite3 initialization by disabling unused features. 2018-04-14 22:07:08 +02:00
Taloth Saldono 740af2751a New: Season Search for Daily series type.
closes #755
2018-04-14 22:07:08 +02:00
Mark McDowall 81e385bebf New: Use media info during import to extract resolution for quality
Closes #448
Closes #1105
2018-04-14 22:01:24 +02:00
nivong 650d18797a Added missing bracket in issue template 2018-04-13 08:48:17 -07:00
Mark McDowall 49e91b8660 Update issue template regarding title prefixes 2018-04-11 20:45:33 -07:00
Taloth Saldono 4fb3d99153 Fixed: Updated AnimeTosho url.
closes #2501
2018-04-11 20:53:32 +02:00
Paul Kozlovitch a29f133e5d Fixed: Pasting title into add new series search input will trigger search 2018-04-03 09:31:49 -07:00
dnnr f277b60cc9 Fix grammar in EditProfileViewTemplate 2018-04-02 15:08:32 +02:00
Taloth Saldono 6d8eebcd30 Revised deletion of cookies. 2018-04-01 20:29:46 +02:00
Taloth Saldono 91c97ed232 Merge branch 'nfo-detector' into develop 2018-03-24 11:54:53 +01:00
margaale 13a259b473 check if mono is running with --debug arg 2018-03-24 11:49:27 +01:00
Taloth Saldono d3f8e0ef4c Fixed: Revised handling of cookies in case of redirects. 2018-03-24 11:43:57 +01:00
Taloth Saldono 700751715b Add form param before submitting request. 2018-03-24 11:43:57 +01:00
Taloth Saldono 58e939beda Fixed: Preserve existing watched status in Kodi nfo files on metadata refreshes (not file upgrades). 2018-03-18 20:10:50 +01:00
Taloth Saldono b19b6c93d3 New: Detect Kodi .nfo vs Scene .nfo and handle as appropriate. Rename scene .nfo to .nfo-orig only when needed. 2018-03-18 20:10:50 +01:00
Taloth Saldono 17e4a8ab66 Moved tests. 2018-03-18 20:10:50 +01:00
Taloth Saldono 3573e631c8 Fixed: Recycle Metadata files on episode removal. 2018-03-18 20:10:50 +01:00
Taloth Saldono 52588509ed Fixed failing test and some flaky tests. 2018-03-16 22:00:59 +01:00
Thirrian e607a67f00 Fixed: Sorting for series "A.P. Bio"
closes #2450
2018-03-12 17:03:59 +01:00
Kevin Richter 96d7382a1c Fixed: Parsing # in front of absolute numbers 2018-03-10 21:41:28 +01:00
Taloth Saldono e15530cee1 Fixed: TheXEM mapping with one scene release to multiple tvdb episodes. 2018-03-09 23:10:30 +01:00
Taloth Saldono 940f59468a New: Required/Ignored restrictions now support /pattern/ regular expressions. 2018-03-09 23:10:29 +01:00
Marcelo Castagna ff885ab3bd Fixed: Added errorcode 160 - Permission denied on FileStation for easier diagnostics 2018-03-09 22:53:15 +01:00
Steven 17cfaf170e Add missing error check when adding a magnet link to deluge (#2295)
* Add missing error check when adding a magnet link to deluge

* Fix typo.
2018-03-09 22:51:52 +01:00
Thirrian f1b2186313 Fix typo 2018-03-09 22:49:54 +01:00
Mark McDowall ac379e3b84 Fixed: Don't add category when removing torrent from qBittorrent
Fixes #2438
2018-03-02 06:32:24 -08:00
Taloth Saldono 616454edb4 Fixed: Preserve existing watched status in Kodi nfo files on metadata refreshes (not file upgrades). 2018-03-01 20:36:07 +01:00
Taloth Saldono d284969379 New: Detect Kodi .nfo vs Scene .nfo and handle as appropriate. Rename scene .nfo to .nfo-orig only when needed. 2018-03-01 20:35:12 +01:00
Taloth Saldono f0eba619f3 Moved tests. 2018-03-01 18:30:09 +01:00
Taloth Saldono 448cfff769 Fixed: Recycle Metadata files on episode removal. 2018-03-01 18:30:08 +01:00
Taloth Saldono 99ee59e9cc Fixed: Updated NLog to 4.5 RC6 to handle mono 5.10 2018-02-27 22:41:46 +01:00
Taloth Saldono c9ff55f601 Added console logging in case NLog fails to initialize. 2018-02-27 22:41:42 +01:00
Mark McDowall ecb0438b16 New: Add MediaInfo to Episode Files returned from the API 2018-02-21 20:40:26 -08:00
Mark McDowall d0acbe992e Log response content from Kodi when checking for errors 2018-02-21 20:39:53 -08:00
Mark McDowall fb44e67ea8 Fixed: Parsing of WEB-DLMux files
Closes #2422
2018-02-21 20:37:25 -08:00
Mark McDowall 3f86c10c44 Fix broken profile test 2018-02-12 17:37:42 -08:00
Mark McDowall 9e7f0c859f New: Opt-in to delete empty series/season folders 2018-02-11 09:01:41 -08:00
Mark McDowall a9c54a1f01 Remove empty series folders when create empty folders is false
New: Remove empty series folders after disk scan/deleting last episode file and create emoty series folders is disabled

Closes #2395
2018-02-10 18:16:59 -08:00
Mark McDowall 1c36bc5fee Return total space when adding new root folder 2018-02-10 10:15:52 -08:00
Mark McDowall b529270f71 Improve error message when deleting a profile that is in use 2018-02-10 10:14:28 -08:00
Mark McDowall bcf45bb68a Fixed: Send category to qBittorrent when adding torrent/magnet 2018-02-10 10:13:35 -08:00
Mark McDowall 9744873e7a Fixed: Removexpost suffix from release groups 2018-02-04 12:22:26 -08:00
Mark McDowall 9a61bb13e6 Fixed: Disable delete button on used quality profiles 2018-02-02 08:54:29 -08:00
Mark McDowall 2f69a158b1 API: Include total space with root folders
Closes #2390
2018-02-01 17:58:55 -08:00
Mark McDowall 4975793b45 Don't reject paths under /srv
Closes #2388
2018-01-31 19:56:04 -08:00
Taloth Saldono d93645fb46 Fixed: Show error if System->Logs fails to load due to ad blocker. 2018-01-27 23:27:52 +01:00
overkill32 d617b6c6e3 Fixed typo in log file 2018-01-27 13:10:41 -08:00
Mark McDowall cc01608f0a Fixed: Show error message when manual import fails to load
Fixes #2384
2018-01-26 22:26:52 -08:00
Mark McDowall 8a3db99811 Fixed: Remove Pre and postbot suffixes from release groups 2018-01-23 19:27:19 -08:00
Mark McDowall 883f7b88cf Removed DailyEpisodeMatchSpecification in favour of SingleEpisodeSearchMatchSpecification
Fixed: Parsing releases with standard and daily formats
2018-01-23 18:53:02 -08:00
Taloth Saldono 03da779ddb Fixed Season Special import. 2018-01-23 16:32:26 +01:00
Taloth Saldono 7c95fc23d0 Fixed Season Special import. 2018-01-22 21:35:20 +01:00
Mark McDowall aa2174b43c Ensure request exists before trying to get query parameters
Fixes #2379
2018-01-21 12:59:00 -08:00
Mark McDowall a023732c1c New: Delay import of episodes without titles temporarily
Closes #2098
2018-01-20 23:56:35 -08:00
Mark McDowall 90f9dce44a Ensure request exists before trying to get query parameters 2018-01-16 12:13:12 -08:00
Mark McDowall ea54b10bf7 First try 2018-01-14 18:12:23 -08:00
Mark McDowall 223ccb5a73 Fixed broken DeleteEpisodeFileFixture tests 2018-01-14 16:23:21 -08:00
Mark McDowall 968d8159a6 Added tests for DB Converters
Closes #482
2018-01-14 12:42:18 -08:00
Mark McDowall 245bf6c7d6 Optionally include season images when fetching series from API
Closes #464
2018-01-13 15:01:33 -08:00
Mark McDowall ee673cd152 Log indexer when processing results
Closes #295
2018-01-13 14:10:19 -08:00
Mark McDowall a1b6095f6e Add Paused above Use SSL for NZBGet 2018-01-13 13:55:28 -08:00
Mark McDowall 5663eb527b Add paused options for Deluge and Transmission
New: Option to add paused for Deluge
New: Option to add paused for Transmission

Closes #795
2018-01-13 11:58:24 -08:00
Mark McDowall 8e8da76467 New: Device names for Join notifications
Closes #2364
2018-01-12 19:14:45 -08:00
Taloth Saldono 8a6acd999a Reordered UI a bit and code cleanup. 2018-01-12 22:00:02 +01:00
Mark McDowall f32e8beab8 Fixed: Parsing of prefixed range multi-episode filenames
Fixes #2359
2018-01-12 12:40:36 -08:00
Marcelo Castagna d25e5fe329 New: Setting for absolute maximum size for a release
Closes #2367 #1996
2018-01-12 20:50:21 +01:00
Taloth d0e8aef949 New: Consider all scene SxxE00 releases Specials as well.
* New: Consider all scene SxxE00 releases Specials as well.

* Cleanup special episode titles and handle E00 specials with existing scenemappings.

* Moved handling of scene mapped E00 to central function.
2018-01-04 21:49:16 +01:00
Kevin Richter ad10349878 Ignore macOS DS_Store files (#2356) 2017-12-31 13:13:02 +01:00
Mark McDowall 560fe31280 csproj fix 2017-12-29 23:37:20 -08:00
Mark McDowall c67a4a61a4 New: Run missing root folder health check when an import is successful
Closes #2309
2017-12-29 22:30:16 -08:00
Mark McDowall 6e85b8782e Fixed: Set air date to 1970-01-01 if episode aired before (mono)
Fixes #2345
2017-12-29 10:56:11 -08:00
Mark McDowall f719c5ccf1 Fixed: Improve logging for invalid NZB messages
Closes #2349
2017-12-28 22:05:20 -08:00
Mark McDowall e8c5e417b6 Improve handling of multiple seasons in one file
Fixed: Invalid scene numbering leading to manual import failing to load
Fixes #2255
2017-12-28 21:48:05 -08:00
Fish2 3492d6bbaa Cleanup moment.js deprecated zone and add functions 2017-12-26 10:10:13 -08:00
Taloth Saldono 15bd181f16 Fixed failing tests in DownloadStation. 2017-12-19 22:49:27 +01:00
Taloth Saldono 96108cb758 Fixed up comments. 2017-12-19 18:38:29 +01:00
Taloth Saldono 459d6ea906 Fixed: Mono internals does not properly copy/move symlinks, but instead copies the contents. 2017-12-19 18:34:53 +01:00
margaale 747f3e171c Fixed: Handling of unknown status types in DownloadStation. 2017-12-19 18:32:48 +01:00
Mark McDowall b371296e78 Validate before deleting series folders
Closes #264
2017-12-16 12:16:33 -08:00
Taloth Saldono 0553a39a02 Rare timing issue on first-use causing duplicate naming config. 2017-12-13 20:25:29 +01:00
Taloth Saldono 4ca5e978ac Fixed MediaCover endpoint. 2017-12-13 20:18:29 +01:00
Mark McDowall 025440ee86 Don't handle content requests in IndexHtmlMapper 2017-12-13 19:16:50 +01:00
Taloth Saldono 40016f2928 Merge branch 'security-patch-2017-1' into develop 2017-12-07 19:44:58 +01:00
Taloth Saldono 581320e3a4 Fixed: Security Vulnerabilities allowing authentication to be bypassed (discovered by Kyle Neideck) 2017-12-07 19:42:18 +01:00
Taloth Saldono 5fae2ac66f Improved handling of the Preflight OPTIONS request. 2017-12-07 19:35:53 +01:00
Taloth Saldono 3ba61cd5aa Fixed: Limit Cross-Origin access to api and specific shared resources. 2017-12-07 19:35:46 +01:00
Taloth Saldono 0008f28236 Fixed: Case sensitivity in handling of static resource names. 2017-12-07 19:35:39 +01:00
Mark McDowall f9ec4c6b0d Updated tests to allow for older versions of mediainfo 2017-12-06 13:22:46 -08:00
Mark McDowall 2b20b91ec5 Updated tests to reflect changes in mediainfo 2017-12-05 14:30:58 -08:00
Mark McDowall e070eeda4a New: Upgrade MediaInfo to 17.10 (Windows/macOS) 2017-12-04 18:52:17 -08:00
Mark McDowall 195a761c29 Fixed: Sorting by episodes on series overview and poster views
Fixes #2167
2017-12-04 18:51:47 -08:00
Mark McDowall 4399d272dc Fixed: Import failures when audio channels are in an unexpected format
Fixes #2318
2017-12-03 19:58:31 -08:00
Mark McDowall 9d7547e941 Add debug logging when formatting audio channels 2017-12-02 16:16:53 -08:00
Mark McDowall 0a690eb4d8 New: Include APFS disks in disk space
Closes #2261
2017-12-01 18:39:44 -08:00
Mark McDowall 32309260b9 Fixed: Sorting Manual Import by relative path
Fixes #2313
2017-11-30 23:18:23 -08:00
Mark McDowall e11e8ad272 New: Channel setting for Slack notifications to override default channel
Closes #2311
2017-11-30 22:48:00 -08:00
Mark McDowall b45b2017a8 Log warnings when deleting an episode file and the root folder is missing/empty
Closes #2305
2017-11-24 18:51:39 -08:00
Mark McDowall ae2a97763d New: Validate NZBs before sending to download client
Closes #263
2017-11-21 19:00:25 -08:00
Frank Scholl 442d49046d New: Add authentication options to Webhook
Closes #2257
2017-11-21 18:55:46 -08:00
Mark McDowall 3d8cd9616e Fixed: Parsing of resolution in TVRips
Fixes #2281
2017-11-21 18:33:25 -08:00
Mark McDowall 9c68c89f24 Fix namespace take 2 2017-11-19 16:09:01 -08:00
Mark McDowall 59ad5f35df Fixed: Parsing when using episode number as folder name in naming config 2017-11-16 17:57:11 -08:00
Mark McDowall 263f3c46b1 Fix namespace 2017-11-16 17:55:55 -08:00
Taloth Saldono 2c13fcf579 Trim quotes from dsm version parts. 2017-11-11 23:20:04 +01:00
Taloth Saldono 7a1c1064ae Fallback to parsing Series from sub path during Manual Import. 2017-11-09 22:10:24 +01:00
Taloth Saldono bb52f3d41c Fixed: Incorrect parsing of filenames with [SDTV] suffix trigging Anime pattern. 2017-11-09 21:54:17 +01:00
Taloth Saldono 0688340722 Fixed: Regression preventing new downloads from bypassing the Download Client Back-off logic.
fixes #2277
2017-11-08 21:29:57 +01:00
406 changed files with 8254 additions and 1865 deletions
+1
View File
@@ -3,6 +3,7 @@ Before opening a new issue, please ensure:
- You use the forums for support/questions - You use the forums for support/questions
- You search for existing bugs/feature requests - You search for existing bugs/feature requests
- Remove extraneous template details - Remove extraneous template details
- Do not prefix title with type of issue (Feature Request, Bug, etc.) The appropriate labels will be added during triage.
--> -->
## Support / Questions ## Support / Questions
+28
View File
@@ -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.
+14
View File
@@ -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.
+7
View File
@@ -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.
+1
View File
@@ -130,6 +130,7 @@ output/*
#OS X metadata files #OS X metadata files
._* ._*
.DS_Store
_start _start
_temp_*/**/* _temp_*/**/*
-11
View File
@@ -52,15 +52,6 @@ CleanFolder()
find $path -depth -empty -type d -exec rm -r "{}" \; find $path -depth -empty -type d -exec rm -r "{}" \;
} }
AddJsonNet()
{
rm $outputFolder/Newtonsoft.Json.*
cp $sourceFolder/packages/Newtonsoft.Json.*/lib/net35/*.dll $outputFolder
cp $sourceFolder/packages/Newtonsoft.Json.*/lib/net35/*.dll $outputFolder/NzbDrone.Update
}
BuildWithMSBuild() BuildWithMSBuild()
{ {
export PATH=$msBuild:$PATH export PATH=$msBuild:$PATH
@@ -91,8 +82,6 @@ Build()
CleanFolder $outputFolder false CleanFolder $outputFolder false
AddJsonNet
echo "Removing Mono.Posix.dll" echo "Removing Mono.Posix.dll"
rm $outputFolder/Mono.Posix.dll rm $outputFolder/Mono.Posix.dll
+4
View File
@@ -9,7 +9,11 @@ APPNAME="Sonarr"
#set up environment #set up environment
if [[ -x '/opt/local/bin/mono' ]]; then if [[ -x '/opt/local/bin/mono' ]]; then
# Macports and mono-supplied installer path
export PATH="/opt/local/bin:$PATH" export PATH="/opt/local/bin:$PATH"
elif [[ -x '/usr/local/bin/mono' ]]; then
# Homebrew-supplied path to mono
export PATH="/usr/local/bin:$PATH"
fi fi
export DYLD_FALLBACK_LIBRARY_PATH="$DIR" export DYLD_FALLBACK_LIBRARY_PATH="$DIR"
Binary file not shown.
Binary file not shown.
+5 -1
View File
@@ -52,10 +52,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net40\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="NLog" version="4.4.12" targetFramework="net40" /> <package id="NLog" version="4.5.3" targetFramework="net40" />
</packages> </packages>
+4 -2
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Data.Common; using System.Data.Common;
@@ -91,9 +91,11 @@ namespace Marr.Data.Mapping
Type entType = ent.GetType(); Type entType = ent.GetType();
if (_repos.Relationships.ContainsKey(entType)) if (_repos.Relationships.ContainsKey(entType))
{ {
var provider = _db.ProviderFactory;
var connectionString = _db.ConnectionString;
Func<IDataMapper> dbCreate = () => Func<IDataMapper> dbCreate = () =>
{ {
var db = new DataMapper(_db.ProviderFactory, _db.ConnectionString); var db = new DataMapper(provider, connectionString);
db.SqlMode = SqlModes.Text; db.SqlMode = SqlModes.Text;
return db; return db;
}; };
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
@@ -21,19 +21,32 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
public void schema_should_have_proper_fields() public void schema_should_have_proper_fields()
{ {
var model = new TestModel var model = new TestModel
{ {
FirstName = "Bob", FirstName = "Bob",
LastName = "Poop" LastName = "Poop"
}; };
var schema = SchemaBuilder.ToSchema(model); 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 == 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 == 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 public class TestModel
{ {
@@ -45,4 +58,13 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
public string Other { get; set; } public string Other { get; set; }
} }
}
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; }
}
}
+7 -1
View File
@@ -7,11 +7,17 @@ namespace NzbDrone.Api.ClientSchema
public int Order { get; set; } public int Order { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Label { get; set; } public string Label { get; set; }
public string Unit { get; set; }
public string HelpText { get; set; } public string HelpText { get; set; }
public string HelpLink { get; set; } public string HelpLink { get; set; }
public object Value { get; set; } public object Value { get; set; }
public string Type { get; set; } public string Type { get; set; }
public bool Advanced { get; set; } public bool Advanced { get; set; }
public List<SelectOption> SelectOptions { get; set; } public List<SelectOption> SelectOptions { get; set; }
public Field Clone()
{
return (Field)MemberwiseClone();
}
} }
} }
@@ -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<object, object> GetterFunc { get; set; }
public Action<object, object> SetterFunc { get; set; }
}
}
+160 -101
View File
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@@ -11,45 +12,22 @@ namespace NzbDrone.Api.ClientSchema
{ {
public static class SchemaBuilder public static class SchemaBuilder
{ {
private static Dictionary<Type, FieldMapping[]> _mappings = new Dictionary<Type, FieldMapping[]>();
public static List<Field> ToSchema(object model) public static List<Field> ToSchema(object model)
{ {
Ensure.That(model, () => model).IsNotNull(); Ensure.That(model, () => model).IsNotNull();
var properties = model.GetType().GetSimpleProperties(); var mappings = GetFieldMappings(model.GetType());
var result = new List<Field>(properties.Count); var result = new List<Field>(mappings.Length);
foreach (var propertyInfo in properties) foreach (var mapping in mappings)
{ {
var fieldAttribute = propertyInfo.GetAttribute<FieldDefinitionAttribute>(false); var field = mapping.Field.Clone();
field.Value = mapping.GetterFunc(model);
if (fieldAttribute != null) result.Add(field);
{
var field = new Field
{
Name = propertyInfo.Name,
Label = fieldAttribute.Label,
HelpText = fieldAttribute.HelpText,
HelpLink = fieldAttribute.HelpLink,
Order = fieldAttribute.Order,
Advanced = fieldAttribute.Advanced,
Type = fieldAttribute.Type.ToString().ToLowerInvariant()
};
var value = propertyInfo.GetValue(model, null);
if (value != null)
{
field.Value = value;
}
if (fieldAttribute.Type == FieldType.Select)
{
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
}
result.Add(field);
}
} }
return result.OrderBy(r => r.Order).ToList(); return result.OrderBy(r => r.Order).ToList();
@@ -59,81 +37,16 @@ namespace NzbDrone.Api.ClientSchema
{ {
Ensure.That(targetType, () => targetType).IsNotNull(); Ensure.That(targetType, () => targetType).IsNotNull();
var properties = targetType.GetSimpleProperties(); var mappings = GetFieldMappings(targetType);
var target = Activator.CreateInstance(targetType); var target = Activator.CreateInstance(targetType);
foreach (var propertyInfo in properties) foreach (var mapping in mappings)
{ {
var fieldAttribute = propertyInfo.GetAttribute<FieldDefinitionAttribute>(false); var propertyType = mapping.PropertyType;
var field = fields.Find(f => f.Name == mapping.Field.Name);
if (fieldAttribute != null) mapping.SetterFunc(target, field.Value);
{
var field = fields.Find(f => f.Name == propertyInfo.Name);
if (propertyInfo.PropertyType == typeof(int))
{
var value = field.Value.ToString().ParseInt32();
propertyInfo.SetValue(target, value ?? 0, null);
}
else if (propertyInfo.PropertyType == typeof(long))
{
var value = field.Value.ToString().ParseInt64();
propertyInfo.SetValue(target, value ?? 0, null);
}
else if (propertyInfo.PropertyType == typeof(int?))
{
var value = field.Value.ToString().ParseInt32();
propertyInfo.SetValue(target, value, null);
}
else if (propertyInfo.PropertyType == typeof(Nullable<Int64>))
{
var value = field.Value.ToString().ParseInt64();
propertyInfo.SetValue(target, value, null);
}
else if (propertyInfo.PropertyType == typeof(IEnumerable<int>))
{
IEnumerable<int> value;
if (field.Value.GetType() == typeof(JArray))
{
value = ((JArray)field.Value).Select(s => s.Value<int>());
}
else
{
value = field.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s));
}
propertyInfo.SetValue(target, value, null);
}
else if (propertyInfo.PropertyType == typeof(IEnumerable<string>))
{
IEnumerable<string> value;
if (field.Value.GetType() == typeof(JArray))
{
value = ((JArray)field.Value).Select(s => s.Value<string>());
}
else
{
value = field.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
propertyInfo.SetValue(target, value, null);
}
else
{
propertyInfo.SetValue(target, field.Value, null);
}
}
} }
return target; return target;
@@ -145,6 +58,84 @@ namespace NzbDrone.Api.ClientSchema
return (T)ReadFromSchema(fields, typeof(T)); return (T)ReadFromSchema(fields, typeof(T));
} }
// Ideally this function should begin a System.Linq.Expression expression tree since it's faster.
// But it's probably not needed till performance issues pop up.
public static FieldMapping[] GetFieldMappings(Type type)
{
lock (_mappings)
{
FieldMapping[] result;
if (!_mappings.TryGetValue(type, out result))
{
result = GetFieldMapping(type, "", v => v);
// Renumber al the field Orders since nested settings will have dupe Orders.
for (int i = 0; i < result.Length; i++)
{
result[i].Field.Order = i;
}
_mappings[type] = result;
}
return result;
}
}
private static FieldMapping[] GetFieldMapping(Type type, string prefix, Func<object, object> targetSelector)
{
var result = new List<FieldMapping>();
foreach (var property in GetProperties(type))
{
var propertyInfo = property.Item1;
if (propertyInfo.PropertyType.IsSimpleType())
{
var fieldAttribute = property.Item2;
var field = new Field
{
Name = prefix + propertyInfo.Name,
Label = fieldAttribute.Label,
Unit = fieldAttribute.Unit,
HelpText = fieldAttribute.HelpText,
HelpLink = fieldAttribute.HelpLink,
Order = fieldAttribute.Order,
Advanced = fieldAttribute.Advanced,
Type = fieldAttribute.Type.ToString().ToLowerInvariant()
};
if (fieldAttribute.Type == FieldType.Select)
{
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
}
var valueConverter = GetValueConverter(propertyInfo.PropertyType);
result.Add(new FieldMapping
{
Field = field,
PropertyType = propertyInfo.PropertyType,
GetterFunc = t => propertyInfo.GetValue(targetSelector(t), null),
SetterFunc = (t, v) => propertyInfo.SetValue(targetSelector(t), valueConverter(v), null)
});
}
else
{
result.AddRange(GetFieldMapping(propertyInfo.PropertyType, propertyInfo.Name + ".", t => propertyInfo.GetValue(targetSelector(t), null)));
}
}
return result.ToArray();
}
private static Tuple<PropertyInfo, FieldDefinitionAttribute>[] GetProperties(Type type)
{
return type.GetProperties()
.Select(v => Tuple.Create(v, v.GetAttribute<FieldDefinitionAttribute>(false)))
.Where(v => v.Item2 != null)
.OrderBy(v => v.Item2.Order)
.ToArray();
}
private static List<SelectOption> GetSelectOptions(Type selectOptions) private static List<SelectOption> GetSelectOptions(Type selectOptions)
{ {
var options = from Enum e in Enum.GetValues(selectOptions) var options = from Enum e in Enum.GetValues(selectOptions)
@@ -152,5 +143,73 @@ namespace NzbDrone.Api.ClientSchema
return options.OrderBy(o => o.Value).ToList(); return options.OrderBy(o => o.Value).ToList();
} }
private static Func<object, object> GetValueConverter(Type propertyType)
{
if (propertyType == typeof(int))
{
return fieldValue => fieldValue?.ToString().ParseInt32() ?? 0;
}
else if (propertyType == typeof(long))
{
return fieldValue => fieldValue?.ToString().ParseInt64() ?? 0;
}
else if (propertyType == typeof(double))
{
return fieldValue => fieldValue?.ToString().ParseDouble() ?? 0.0;
}
else if (propertyType == typeof(int?))
{
return fieldValue => fieldValue?.ToString().ParseInt32();
}
else if (propertyType == typeof(Int64?))
{
return fieldValue => fieldValue?.ToString().ParseInt64();
}
else if (propertyType == typeof(double?))
{
return fieldValue => fieldValue?.ToString().ParseDouble();
}
else if (propertyType == typeof(IEnumerable<int>))
{
return fieldValue =>
{
if (fieldValue.GetType() == typeof(JArray))
{
return ((JArray)fieldValue).Select(s => s.Value<int>());
}
else
{
return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s));
}
};
}
else if (propertyType == typeof(IEnumerable<string>))
{
return fieldValue =>
{
if (fieldValue.GetType() == typeof(JArray))
{
return ((JArray)fieldValue).Select(s => s.Value<string>());
}
else
{
return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
};
}
else
{
return fieldValue => fieldValue;
}
}
} }
} }
+27 -2
View File
@@ -4,6 +4,7 @@ using System.Linq;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
using NzbDrone.Api.Validation; using NzbDrone.Api.Validation;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@@ -17,6 +18,8 @@ namespace NzbDrone.Api.Commands
{ {
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
private readonly IServiceFactory _serviceFactory; private readonly IServiceFactory _serviceFactory;
private readonly Debouncer _debouncer;
private readonly Dictionary<int, CommandResource> _pendingUpdates;
public CommandModule(IManageCommandQueue commandQueueManager, public CommandModule(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster, IBroadcastSignalRMessage signalRBroadcaster,
@@ -31,6 +34,10 @@ namespace NzbDrone.Api.Commands
GetResourceAll = GetStartedCommands; GetResourceAll = GetStartedCommands;
PostValidator.RuleFor(c => c.Name).NotBlank(); PostValidator.RuleFor(c => c.Name).NotBlank();
_debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1));
_pendingUpdates = new Dictionary<int, CommandResource>();
} }
private CommandResource GetCommand(int id) private CommandResource GetCommand(int id)
@@ -59,8 +66,26 @@ namespace NzbDrone.Api.Commands
{ {
if (message.Command.Body.SendUpdatesToClient) if (message.Command.Body.SendUpdatesToClient)
{ {
BroadcastResourceChange(ModelAction.Updated, message.Command.ToResource()); lock (_pendingUpdates)
{
_pendingUpdates[message.Command.Id] = message.Command.ToResource();
}
_debouncer.Execute();
}
}
private void SendUpdates()
{
lock (_pendingUpdates)
{
var pendingUpdates = _pendingUpdates.Values.ToArray();
_pendingUpdates.Clear();
foreach (var pendingUpdate in pendingUpdates)
{
BroadcastResourceChange(ModelAction.Updated, pendingUpdate);
}
} }
} }
} }
} }
@@ -13,6 +13,9 @@ namespace NzbDrone.Api.Config
SharedValidator.RuleFor(c => c.MinimumAge) SharedValidator.RuleFor(c => c.MinimumAge)
.GreaterThanOrEqualTo(0); .GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.MaximumSize)
.GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.Retention) SharedValidator.RuleFor(c => c.Retention)
.GreaterThanOrEqualTo(0); .GreaterThanOrEqualTo(0);
@@ -25,4 +28,4 @@ namespace NzbDrone.Api.Config
return IndexerConfigResourceMapper.ToResource(model); return IndexerConfigResourceMapper.ToResource(model);
} }
} }
} }
@@ -6,6 +6,7 @@ namespace NzbDrone.Api.Config
public class IndexerConfigResource : RestResource public class IndexerConfigResource : RestResource
{ {
public int MinimumAge { get; set; } public int MinimumAge { get; set; }
public int MaximumSize { get; set; }
public int Retention { get; set; } public int Retention { get; set; }
public int RssSyncInterval { get; set; } public int RssSyncInterval { get; set; }
} }
@@ -17,6 +18,7 @@ namespace NzbDrone.Api.Config
return new IndexerConfigResource return new IndexerConfigResource
{ {
MinimumAge = model.MinimumAge, MinimumAge = model.MinimumAge,
MaximumSize = model.MaximumSize,
Retention = model.Retention, Retention = model.Retention,
RssSyncInterval = model.RssSyncInterval, RssSyncInterval = model.RssSyncInterval,
}; };
@@ -1,4 +1,4 @@
using NzbDrone.Api.REST; using NzbDrone.Api.REST;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@@ -10,6 +10,7 @@ namespace NzbDrone.Api.Config
public string RecycleBin { get; set; } public string RecycleBin { get; set; }
public bool AutoDownloadPropers { get; set; } public bool AutoDownloadPropers { get; set; }
public bool CreateEmptySeriesFolders { get; set; } public bool CreateEmptySeriesFolders { get; set; }
public bool DeleteEmptyFolders { get; set; }
public FileDateType FileDate { get; set; } public FileDateType FileDate { get; set; }
public bool SetPermissionsLinux { get; set; } public bool SetPermissionsLinux { get; set; }
@@ -35,6 +36,7 @@ namespace NzbDrone.Api.Config
RecycleBin = model.RecycleBin, RecycleBin = model.RecycleBin,
AutoDownloadPropers = model.AutoDownloadPropers, AutoDownloadPropers = model.AutoDownloadPropers,
CreateEmptySeriesFolders = model.CreateEmptySeriesFolders, CreateEmptySeriesFolders = model.CreateEmptySeriesFolders,
DeleteEmptyFolders = model.DeleteEmptyFolders,
FileDate = model.FileDate, FileDate = model.FileDate,
SetPermissionsLinux = model.SetPermissionsLinux, SetPermissionsLinux = model.SetPermissionsLinux,
@@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using NzbDrone.Api.REST; using NzbDrone.Api.REST;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
@@ -15,6 +15,8 @@ namespace NzbDrone.Api.EpisodeFiles
public DateTime DateAdded { get; set; } public DateTime DateAdded { get; set; }
public string SceneName { get; set; } public string SceneName { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public MediaInfoResource MediaInfo { get; set; }
public string OriginalFilePath { get; set; }
public bool QualityCutoffNotMet { get; set; } public bool QualityCutoffNotMet { get; set; }
} }
@@ -37,7 +39,8 @@ namespace NzbDrone.Api.EpisodeFiles
DateAdded = model.DateAdded, DateAdded = model.DateAdded,
SceneName = model.SceneName, SceneName = model.SceneName,
Quality = model.Quality, Quality = model.Quality,
//QualityCutoffNotMet MediaInfo = model.MediaInfo.ToResource(model.SceneName),
OriginalFilePath = model.OriginalFilePath
}; };
} }
@@ -57,7 +60,9 @@ namespace NzbDrone.Api.EpisodeFiles
DateAdded = model.DateAdded, DateAdded = model.DateAdded,
SceneName = model.SceneName, SceneName = model.SceneName,
Quality = model.Quality, Quality = model.Quality,
QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(series.Profile.Value, model.Quality) QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(series.Profile.Value, model.Quality),
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
OriginalFilePath = model.OriginalFilePath
}; };
} }
} }
@@ -0,0 +1,30 @@
using NzbDrone.Api.REST;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Api.EpisodeFiles
{
public class MediaInfoResource : RestResource
{
public decimal AudioChannels { get; set; }
public string AudioCodec { get; set; }
public string VideoCodec { get; set; }
}
public static class MediaInfoResourceMapper
{
public static MediaInfoResource ToResource(this MediaInfoModel model, string sceneName)
{
if (model == null)
{
return null;
}
return new MediaInfoResource
{
AudioChannels = MediaInfoFormatter.FormatAudioChannels(model),
AudioCodec = MediaInfoFormatter.FormatAudioCodec(model, sceneName),
VideoCodec = MediaInfoFormatter.FormatVideoCodec(model, sceneName)
};
}
}
}
+3 -1
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -30,6 +30,7 @@ namespace NzbDrone.Api.Episodes
public bool UnverifiedSceneNumbering { get; set; } public bool UnverifiedSceneNumbering { get; set; }
public string SeriesTitle { get; set; } public string SeriesTitle { get; set; }
public SeriesResource Series { get; set; } public SeriesResource Series { get; set; }
public DateTime? LastSearchTime { get; set; }
//Hiding this so people don't think its usable (only used to set the initial state) //Hiding this so people don't think its usable (only used to set the initial state)
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
@@ -65,6 +66,7 @@ namespace NzbDrone.Api.Episodes
UnverifiedSceneNumbering = model.UnverifiedSceneNumbering, UnverifiedSceneNumbering = model.UnverifiedSceneNumbering,
SeriesTitle = model.SeriesTitle, SeriesTitle = model.SeriesTitle,
//Series = model.Series.MapToResource(), //Series = model.Series.MapToResource(),
LastSearchTime = model.LastSearchTime
}; };
} }
@@ -23,6 +23,8 @@ namespace NzbDrone.Api.Extensions.Pipelines
private void Handle(NancyContext context) private void Handle(NancyContext context)
{ {
if (context.Request.Method == "OPTIONS") return;
if (_cacheableSpecification.IsCacheable(context)) if (_cacheableSpecification.IsCacheable(context))
{ {
context.Response.Headers.EnableCache(); context.Response.Headers.EnableCache();
@@ -33,4 +35,4 @@ namespace NzbDrone.Api.Extensions.Pipelines
} }
} }
} }
} }
@@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using Nancy; using Nancy;
using Nancy.Bootstrapper; using Nancy.Bootstrapper;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Api.Extensions.Pipelines namespace NzbDrone.Api.Extensions.Pipelines
{ {
@@ -11,10 +12,25 @@ namespace NzbDrone.Api.Extensions.Pipelines
public void Register(IPipelines pipelines) public void Register(IPipelines pipelines)
{ {
pipelines.AfterRequest.AddItemToEndOfPipeline((Action<NancyContext>) Handle); pipelines.BeforeRequest.AddItemToEndOfPipeline(HandleRequest);
pipelines.AfterRequest.AddItemToEndOfPipeline(HandleResponse);
} }
private void Handle(NancyContext context) private Response HandleRequest(NancyContext context)
{
if (context == null || context.Request.Method != "OPTIONS")
{
return null;
}
var response = new Response()
.WithStatusCode(HttpStatusCode.OK)
.WithContentType("");
ApplyResponseHeaders(response, context.Request);
return response;
}
private void HandleResponse(NancyContext context)
{ {
if (context == null || context.Response.Headers.ContainsKey(AccessControlHeaders.AllowOrigin)) if (context == null || context.Response.Headers.ContainsKey(AccessControlHeaders.AllowOrigin))
{ {
@@ -26,21 +42,39 @@ namespace NzbDrone.Api.Extensions.Pipelines
private static void ApplyResponseHeaders(Response response, Request request) private static void ApplyResponseHeaders(Response response, Request request)
{ {
var allowedMethods = "GET, OPTIONS, PATCH, POST, PUT, DELETE"; if (request.IsApiRequest())
if (response.Headers.ContainsKey("Allow"))
{ {
allowedMethods = response.Headers["Allow"]; // Allow Cross-Origin access to the API since it's protected with the apikey, and nothing else.
ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS, PATCH, POST, PUT, DELETE");
} }
else if (request.IsSharedContentRequest())
var requestedHeaders = string.Join(", ", request.Headers[AccessControlHeaders.RequestHeaders]);
response.Headers.Add(AccessControlHeaders.AllowOrigin, "*");
response.Headers.Add(AccessControlHeaders.AllowMethods, allowedMethods);
if (request.Headers[AccessControlHeaders.RequestHeaders].Any())
{ {
response.Headers.Add(AccessControlHeaders.AllowHeaders, requestedHeaders); // Allow Cross-Origin access to specific shared content such as mediacovers and images.
ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS");
}
// Disallow Cross-Origin access for any other route.
}
private static void ApplyCorsResponseHeaders(Response response, Request request, string allowOrigin, string allowedMethods)
{
response.Headers.Add(AccessControlHeaders.AllowOrigin, allowOrigin);
if (request.Method == "OPTIONS")
{
if (response.Headers.ContainsKey("Allow"))
{
allowedMethods = response.Headers["Allow"];
}
response.Headers.Add(AccessControlHeaders.AllowMethods, allowedMethods);
if (request.Headers[AccessControlHeaders.RequestHeaders].Any())
{
var requestedHeaders = request.Headers[AccessControlHeaders.RequestHeaders].Join(", ");
response.Headers.Add(AccessControlHeaders.AllowHeaders, requestedHeaders);
}
} }
} }
} }
@@ -5,6 +5,7 @@ using System.Linq;
using Nancy; using Nancy;
using Nancy.Bootstrapper; using Nancy.Bootstrapper;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
namespace NzbDrone.Api.Extensions.Pipelines namespace NzbDrone.Api.Extensions.Pipelines
@@ -15,9 +16,14 @@ namespace NzbDrone.Api.Extensions.Pipelines
public int Order => 0; public int Order => 0;
private readonly Action<Action<Stream>, Stream> _writeGZipStream;
public GzipCompressionPipeline(Logger logger) public GzipCompressionPipeline(Logger logger)
{ {
_logger = logger; _logger = logger;
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
_writeGZipStream = PlatformInfo.IsMono ? WriteGZipStreamMono : (Action<Action<Stream>, Stream>)WriteGZipStream;
} }
public void Register(IPipelines pipelines) public void Register(IPipelines pipelines)
@@ -33,7 +39,8 @@ namespace NzbDrone.Api.Extensions.Pipelines
try try
{ {
if ( if (
!response.ContentType.Contains("image") response.Contents != Response.NoBody
&& !response.ContentType.Contains("image")
&& !response.ContentType.Contains("font") && !response.ContentType.Contains("font")
&& request.Headers.AcceptEncoding.Any(x => x.Contains("gzip")) && request.Headers.AcceptEncoding.Any(x => x.Contains("gzip"))
&& !AlreadyGzipEncoded(response) && !AlreadyGzipEncoded(response)
@@ -42,14 +49,7 @@ namespace NzbDrone.Api.Extensions.Pipelines
var contents = response.Contents; var contents = response.Contents;
response.Headers["Content-Encoding"] = "gzip"; response.Headers["Content-Encoding"] = "gzip";
response.Contents = responseStream => response.Contents = responseStream => _writeGZipStream(contents, responseStream);
{
using (var gzip = new GZipStream(responseStream, CompressionMode.Compress, true))
using (var buffered = new BufferedStream(gzip, 8192))
{
contents.Invoke(buffered);
}
};
} }
} }
@@ -60,6 +60,25 @@ namespace NzbDrone.Api.Extensions.Pipelines
} }
} }
private static void WriteGZipStreamMono(Action<Stream> innerContent, Stream targetStream)
{
using (var membuffer = new MemoryStream())
{
WriteGZipStream(innerContent, membuffer);
membuffer.Position = 0;
membuffer.CopyTo(targetStream);
}
}
private static void WriteGZipStream(Action<Stream> innerContent, Stream targetStream)
{
using (var gzip = new GZipStream(targetStream, CompressionMode.Compress, true))
using (var buffered = new BufferedStream(gzip, 8192))
{
innerContent.Invoke(buffered);
}
}
private static bool ContentLengthIsTooSmall(Response response) private static bool ContentLengthIsTooSmall(Response response)
{ {
var contentLength = response.Headers.GetValueOrDefault("Content-Length"); var contentLength = response.Headers.GetValueOrDefault("Content-Length");
@@ -80,4 +99,4 @@ namespace NzbDrone.Api.Extensions.Pipelines
return false; return false;
} }
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using Nancy; using Nancy;
namespace NzbDrone.Api.Extensions namespace NzbDrone.Api.Extensions
@@ -36,5 +36,23 @@ namespace NzbDrone.Api.Extensions
{ {
return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase); return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase);
} }
public static bool IsSharedContentRequest(this Request request)
{
return request.Path.StartsWith("/MediaCover/", StringComparison.InvariantCultureIgnoreCase) ||
request.Path.StartsWith("/Content/Images/", StringComparison.InvariantCultureIgnoreCase);
}
public static bool GetBooleanQueryParameter(this Request request, string parameter, bool defaultValue = false)
{
var parameterValue = request.Query[parameter];
if (parameterValue.HasValue)
{
return bool.Parse(parameterValue.Value);
}
return defaultValue;
}
} }
} }
@@ -49,7 +49,12 @@ namespace NzbDrone.Api.Frontend.Mappers
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
{ {
return !resourceUrl.Contains(".") && !resourceUrl.StartsWith("/login"); resourceUrl = resourceUrl.ToLowerInvariant();
return !resourceUrl.StartsWith("/content") &&
!resourceUrl.StartsWith("/mediacover") &&
!resourceUrl.Contains(".") &&
!resourceUrl.StartsWith("/login");
} }
public override Response GetResponse(string resourceUrl) public override Response GetResponse(string resourceUrl)
@@ -113,4 +118,4 @@ namespace NzbDrone.Api.Frontend.Mappers
return _generatedContent; return _generatedContent;
} }
} }
} }
@@ -1,3 +1,4 @@
using System;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NLog; using NLog;
@@ -42,7 +43,7 @@ namespace NzbDrone.Api.Frontend.Mappers
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
{ {
return resourceUrl.StartsWith("/MediaCover"); return resourceUrl.StartsWith("/MediaCover", StringComparison.InvariantCultureIgnoreCase);
} }
} }
} }
@@ -1,3 +1,4 @@
using System;
using System.IO; using System.IO;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@@ -28,7 +29,9 @@ namespace NzbDrone.Api.Frontend.Mappers
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
{ {
return resourceUrl.StartsWith("/Content") || resourceUrl = resourceUrl.ToLowerInvariant();
return resourceUrl.StartsWith("/content") ||
resourceUrl.EndsWith(".js") || resourceUrl.EndsWith(".js") ||
resourceUrl.EndsWith(".map") || resourceUrl.EndsWith(".map") ||
resourceUrl.EndsWith(".css") || resourceUrl.EndsWith(".css") ||
@@ -37,4 +40,4 @@ namespace NzbDrone.Api.Frontend.Mappers
resourceUrl.EndsWith("oauth.html"); resourceUrl.EndsWith("oauth.html");
} }
} }
} }
@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy.Responses;
using NLog; using NLog;
using Nancy; using Nancy;
using NzbDrone.Api.Frontend.Mappers; using NzbDrone.Api.Frontend.Mappers;
+49 -6
View File
@@ -1,13 +1,16 @@
using Nancy; using System.Collections.Generic;
using Nancy.ModelBinding; using System.Linq;
using FluentValidation; using FluentValidation;
using Nancy;
using Nancy.ModelBinding;
using NLog;
using NzbDrone.Api.Extensions;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using System.Collections.Generic; using NzbDrone.Core.Indexers;
using System.Linq;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Api.Extensions;
using NLog;
namespace NzbDrone.Api.Indexers namespace NzbDrone.Api.Indexers
{ {
@@ -15,14 +18,17 @@ namespace NzbDrone.Api.Indexers
{ {
private readonly IMakeDownloadDecision _downloadDecisionMaker; private readonly IMakeDownloadDecision _downloadDecisionMaker;
private readonly IProcessDownloadDecisions _downloadDecisionProcessor; private readonly IProcessDownloadDecisions _downloadDecisionProcessor;
private readonly IIndexerFactory _indexerFactory;
private readonly Logger _logger; private readonly Logger _logger;
public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker, public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker,
IProcessDownloadDecisions downloadDecisionProcessor, IProcessDownloadDecisions downloadDecisionProcessor,
IIndexerFactory indexerFactory,
Logger logger) Logger logger)
{ {
_downloadDecisionMaker = downloadDecisionMaker; _downloadDecisionMaker = downloadDecisionMaker;
_downloadDecisionProcessor = downloadDecisionProcessor; _downloadDecisionProcessor = downloadDecisionProcessor;
_indexerFactory = indexerFactory;
_logger = logger; _logger = logger;
Post["/push"] = x => ProcessRelease(this.Bind<ReleaseResource>()); Post["/push"] = x => ProcessRelease(this.Bind<ReleaseResource>());
@@ -41,10 +47,47 @@ namespace NzbDrone.Api.Indexers
info.Guid = "PUSH-" + info.DownloadUrl; info.Guid = "PUSH-" + info.DownloadUrl;
ResolveIndexer(info);
var decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info }); var decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info });
_downloadDecisionProcessor.ProcessDecisions(decisions); _downloadDecisionProcessor.ProcessDecisions(decisions);
return MapDecisions(decisions).First().AsResponse(); return MapDecisions(decisions).First().AsResponse();
} }
private void ResolveIndexer(ReleaseInfo release)
{
if (release.IndexerId == 0 && release.Indexer.IsNotNullOrWhiteSpace())
{
var indexer = _indexerFactory.All().FirstOrDefault(v => v.Name == release.Indexer);
if (indexer != null)
{
release.IndexerId = indexer.Id;
_logger.Debug("Push Release {0} associated with indexer {1} - {2}.", release.Title, release.IndexerId, release.Indexer);
}
else
{
_logger.Debug("Push Release {0} not associated with unknown indexer {1}.", release.Title, release.Indexer);
}
}
else if (release.IndexerId != 0 && release.Indexer.IsNullOrWhiteSpace())
{
try
{
var indexer = _indexerFactory.Get(release.IndexerId);
release.Indexer = indexer.Name;
_logger.Debug("Push Release {0} associated with indexer {1} - {2}.", release.Title, release.IndexerId, release.Indexer);
}
catch (ModelNotFoundException)
{
_logger.Debug("Push Release {0} not associated with unknown indexer {0}.", release.Title, release.IndexerId);
release.IndexerId = 0;
}
}
else
{
_logger.Debug("Push Release {0} not associated with an indexer.", release.Title);
}
}
} }
} }
@@ -13,6 +13,7 @@ namespace NzbDrone.Api.ManualImport
{ {
public string Path { get; set; } public string Path { get; set; }
public string RelativePath { get; set; } public string RelativePath { get; set; }
public string FolderName { get; set; }
public string Name { get; set; } public string Name { get; set; }
public long Size { get; set; } public long Size { get; set; }
public SeriesResource Series { get; set; } public SeriesResource Series { get; set; }
@@ -36,6 +37,7 @@ namespace NzbDrone.Api.ManualImport
Path = model.Path, Path = model.Path,
RelativePath = model.RelativePath, RelativePath = model.RelativePath,
FolderName = model.FolderName,
Name = model.Name, Name = model.Name,
Size = model.Size, Size = model.Size,
Series = model.Series.ToResource(), Series = model.Series.ToResource(),
+8 -1
View File
@@ -70,12 +70,13 @@
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net40\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference> </Reference>
<Reference Include="NodaTime, Version=1.3.0.0, Culture=neutral, PublicKeyToken=4226afe0d9b296d1, processorArchitecture=MSIL"> <Reference Include="NodaTime, Version=1.3.0.0, Culture=neutral, PublicKeyToken=4226afe0d9b296d1, processorArchitecture=MSIL">
<HintPath>..\packages\Ical.Net.2.2.32\lib\net40\NodaTime.dll</HintPath> <HintPath>..\packages\Ical.Net.2.2.32\lib\net40\NodaTime.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
@@ -83,6 +84,10 @@
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\Libraries\Sqlite\System.Data.SQLite.dll</HintPath> <HintPath>..\Libraries\Sqlite\System.Data.SQLite.dll</HintPath>
</Reference> </Reference>
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs"> <Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs">
@@ -99,11 +104,13 @@
<Compile Include="Calendar\CalendarModule.cs" /> <Compile Include="Calendar\CalendarModule.cs" />
<Compile Include="ClientSchema\Field.cs" /> <Compile Include="ClientSchema\Field.cs" />
<Compile Include="ClientSchema\FieldDefinitionAttribute.cs" /> <Compile Include="ClientSchema\FieldDefinitionAttribute.cs" />
<Compile Include="ClientSchema\FieldMapping.cs" />
<Compile Include="ClientSchema\SchemaBuilder.cs" /> <Compile Include="ClientSchema\SchemaBuilder.cs" />
<Compile Include="ClientSchema\SchemaDeserializer.cs" /> <Compile Include="ClientSchema\SchemaDeserializer.cs" />
<Compile Include="ClientSchema\SelectOption.cs" /> <Compile Include="ClientSchema\SelectOption.cs" />
<Compile Include="Commands\CommandModule.cs" /> <Compile Include="Commands\CommandModule.cs" />
<Compile Include="Commands\CommandResource.cs" /> <Compile Include="Commands\CommandResource.cs" />
<Compile Include="EpisodeFiles\MediaInfoResource.cs" />
<Compile Include="Extensions\AccessControlHeaders.cs" /> <Compile Include="Extensions\AccessControlHeaders.cs" />
<Compile Include="Extensions\Pipelines\CorsPipeline.cs" /> <Compile Include="Extensions\Pipelines\CorsPipeline.cs" />
<Compile Include="Extensions\Pipelines\UrlBasePipeline.cs" /> <Compile Include="Extensions\Pipelines\UrlBasePipeline.cs" />
+6 -1
View File
@@ -210,7 +210,12 @@ namespace NzbDrone.Api
protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings) 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)) if (includeWarnings && (!result.IsValid || result.HasWarnings))
{ {
@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
@@ -17,7 +17,9 @@ namespace NzbDrone.Api.RootFolders
DroneFactoryValidator droneFactoryValidator, DroneFactoryValidator droneFactoryValidator,
MappedNetworkDriveValidator mappedNetworkDriveValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator,
StartupFolderValidator startupFolderValidator, StartupFolderValidator startupFolderValidator,
FolderWritableValidator folderWritableValidator) SystemFolderValidator systemFolderValidator,
FolderWritableValidator folderWritableValidator
)
: base(signalRBroadcaster) : base(signalRBroadcaster)
{ {
_rootFolderService = rootFolderService; _rootFolderService = rootFolderService;
@@ -35,6 +37,7 @@ namespace NzbDrone.Api.RootFolders
.SetValidator(mappedNetworkDriveValidator) .SetValidator(mappedNetworkDriveValidator)
.SetValidator(startupFolderValidator) .SetValidator(startupFolderValidator)
.SetValidator(pathExistsValidator) .SetValidator(pathExistsValidator)
.SetValidator(systemFolderValidator)
.SetValidator(folderWritableValidator); .SetValidator(folderWritableValidator);
} }
@@ -60,4 +63,4 @@ namespace NzbDrone.Api.RootFolders
_rootFolderService.Remove(id); _rootFolderService.Remove(id);
} }
} }
} }
@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Api.REST; using NzbDrone.Api.REST;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
@@ -9,6 +9,7 @@ namespace NzbDrone.Api.RootFolders
{ {
public string Path { get; set; } public string Path { get; set; }
public long? FreeSpace { get; set; } public long? FreeSpace { get; set; }
public long? TotalSpace { get; set; }
public List<UnmappedFolder> UnmappedFolders { get; set; } public List<UnmappedFolder> UnmappedFolders { get; set; }
} }
@@ -25,6 +26,7 @@ namespace NzbDrone.Api.RootFolders
Path = model.Path, Path = model.Path,
FreeSpace = model.FreeSpace, FreeSpace = model.FreeSpace,
TotalSpace = model.TotalSpace,
UnmappedFolders = model.UnmappedFolders UnmappedFolders = model.UnmappedFolders
}; };
} }
@@ -48,4 +50,4 @@ namespace NzbDrone.Api.RootFolders
return models.Select(ToResource).ToList(); return models.Select(ToResource).ToList();
} }
} }
} }
+10 -6
View File
@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Api.Series namespace NzbDrone.Api.Series
{ {
@@ -8,18 +9,20 @@ namespace NzbDrone.Api.Series
public int SeasonNumber { get; set; } public int SeasonNumber { get; set; }
public bool Monitored { get; set; } public bool Monitored { get; set; }
public SeasonStatisticsResource Statistics { get; set; } public SeasonStatisticsResource Statistics { get; set; }
public List<MediaCover> Images { get; set; }
} }
public static class SeasonResourceMapper 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; if (model == null) return null;
return new SeasonResource return new SeasonResource
{ {
SeasonNumber = model.SeasonNumber, 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 return new Season
{ {
SeasonNumber = resource.SeasonNumber, SeasonNumber = resource.SeasonNumber,
Monitored = resource.Monitored Monitored = resource.Monitored,
Images = resource.Images
}; };
} }
public static List<SeasonResource> ToResource(this IEnumerable<Season> models) public static List<SeasonResource> ToResource(this IEnumerable<Season> models, bool includeImages = false)
{ {
return models.Select(ToResource).ToList(); return models.Select(s => ToResource(s, includeImages)).ToList();
} }
public static List<Season> ToModel(this IEnumerable<SeasonResource> resources) public static List<Season> ToModel(this IEnumerable<SeasonResource> resources)
@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy; using Nancy;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
@@ -24,7 +24,7 @@ namespace NzbDrone.Api.Series
var series = resources.Select(seriesResource => seriesResource.ToModel(_seriesService.GetSeries(seriesResource.Id))).ToList(); var series = resources.Select(seriesResource => seriesResource.ToModel(_seriesService.GetSeries(seriesResource.Id))).ToList();
return _seriesService.UpdateSeries(series) return _seriesService.UpdateSeries(series)
.ToResource() .ToResource(false)
.AsResponse(HttpStatusCode.Accepted); .AsResponse(HttpStatusCode.Accepted);
} }
} }
+21 -15
View File
@@ -1,7 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation; using FluentValidation;
using NzbDrone.Api.Extensions;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
@@ -45,6 +46,7 @@ namespace NzbDrone.Api.Series
SeriesExistsValidator seriesExistsValidator, SeriesExistsValidator seriesExistsValidator,
DroneFactoryValidator droneFactoryValidator, DroneFactoryValidator droneFactoryValidator,
SeriesAncestorValidator seriesAncestorValidator, SeriesAncestorValidator seriesAncestorValidator,
SystemFolderValidator systemFolderValidator,
ProfileExistsValidator profileExistsValidator ProfileExistsValidator profileExistsValidator
) )
: base(signalRBroadcaster) : base(signalRBroadcaster)
@@ -71,6 +73,7 @@ namespace NzbDrone.Api.Series
.SetValidator(seriesPathValidator) .SetValidator(seriesPathValidator)
.SetValidator(droneFactoryValidator) .SetValidator(droneFactoryValidator)
.SetValidator(seriesAncestorValidator) .SetValidator(seriesAncestorValidator)
.SetValidator(systemFolderValidator)
.When(s => !s.Path.IsNullOrWhiteSpace()); .When(s => !s.Path.IsNullOrWhiteSpace());
SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator); SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator);
@@ -84,26 +87,17 @@ namespace NzbDrone.Api.Series
private SeriesResource GetSeries(int id) private SeriesResource GetSeries(int id)
{ {
var includeSeasonImages = Context != null && Request.GetBooleanQueryParameter("includeSeasonImages");
var series = _seriesService.GetSeries(id); var series = _seriesService.GetSeries(id);
return MapToResource(series); return MapToResource(series, includeSeasonImages);
}
private SeriesResource MapToResource(Core.Tv.Series series)
{
if (series == null) return null;
var resource = series.ToResource();
MapCoversToLocal(resource);
FetchAndLinkSeriesStatistics(resource);
PopulateAlternateTitles(resource);
return resource;
} }
private List<SeriesResource> AllSeries() private List<SeriesResource> AllSeries()
{ {
var includeSeasonImages = Request.GetBooleanQueryParameter("includeSeasonImages");
var seriesStats = _seriesStatisticsService.SeriesStatistics(); var seriesStats = _seriesStatisticsService.SeriesStatistics();
var seriesResources = _seriesService.GetAllSeries().ToResource(); var seriesResources = _seriesService.GetAllSeries().Select(s => s.ToResource(includeSeasonImages)).ToList();
MapCoversToLocal(seriesResources.ToArray()); MapCoversToLocal(seriesResources.ToArray());
LinkSeriesStatistics(seriesResources, seriesStats); LinkSeriesStatistics(seriesResources, seriesStats);
@@ -141,6 +135,18 @@ namespace NzbDrone.Api.Series
_seriesService.DeleteSeries(id, deleteFiles); _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) private void MapCoversToLocal(params SeriesResource[] series)
{ {
foreach (var seriesResource in series) foreach (var seriesResource in series)
+5 -5
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Api.REST; using NzbDrone.Api.REST;
@@ -97,7 +97,7 @@ namespace NzbDrone.Api.Series
public static class SeriesResourceMapper 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; if (model == null) return null;
@@ -121,7 +121,7 @@ namespace NzbDrone.Api.Series
AirTime = model.AirTime, AirTime = model.AirTime,
Images = model.Images, Images = model.Images,
Seasons = model.Seasons.ToResource(), Seasons = model.Seasons.ToResource(includeSeasonImages),
Year = model.Year, Year = model.Year,
Path = model.Path, Path = model.Path,
@@ -214,9 +214,9 @@ namespace NzbDrone.Api.Series
return series; return series;
} }
public static List<SeriesResource> ToResource(this IEnumerable<Core.Tv.Series> series) public static List<SeriesResource> ToResource(this IEnumerable<Core.Tv.Series> series, bool includeSeasonImages)
{ {
return series.Select(ToResource).ToList(); return series.Select(s => ToResource(s, includeSeasonImages)).ToList();
} }
} }
} }
+1 -1
View File
@@ -6,5 +6,5 @@
<package id="Nancy.Authentication.Basic" version="1.4.1" targetFramework="net40" /> <package id="Nancy.Authentication.Basic" version="1.4.1" targetFramework="net40" />
<package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net40" /> <package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net40" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" />
<package id="NLog" version="4.4.12" targetFramework="net40" /> <package id="NLog" version="4.5.3" targetFramework="net40" />
</packages> </packages>
@@ -47,19 +47,25 @@
<Reference Include="FluentAssertions.Core, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL"> <Reference Include="FluentAssertions.Core, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL">
<HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath> <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath>
</Reference> </Reference>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath> <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net40\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference> </Reference>
<Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> <Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath> <HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceProcess" /> <Reference Include="System.ServiceProcess" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
</ItemGroup> </ItemGroup>
+1 -1
View File
@@ -3,6 +3,6 @@
<package id="FluentAssertions" version="4.19.0" targetFramework="net40" /> <package id="FluentAssertions" version="4.19.0" targetFramework="net40" />
<package id="Moq" version="4.0.10827" targetFramework="net40" /> <package id="Moq" version="4.0.10827" targetFramework="net40" />
<package id="NBuilder" version="4.0.0" targetFramework="net40" /> <package id="NBuilder" version="4.0.0" targetFramework="net40" />
<package id="NLog" version="4.4.12" targetFramework="net40" /> <package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="NUnit" version="3.6.0" targetFramework="net40" /> <package id="NUnit" version="3.6.0" targetFramework="net40" />
</packages> </packages>
@@ -45,16 +45,20 @@
<HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath> <HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net40\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference> </Reference>
<Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> <Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath> <HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="FluentAssertions" version="4.19.0" targetFramework="net40" /> <package id="FluentAssertions" version="4.19.0" targetFramework="net40" />
<package id="NLog" version="4.4.12" targetFramework="net40" /> <package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="NUnit" version="3.6.0" targetFramework="net40" /> <package id="NUnit" version="3.6.0" targetFramework="net40" />
<package id="Selenium.Support" version="3.2.0" targetFramework="net40" /> <package id="Selenium.Support" version="3.2.0" targetFramework="net40" />
<package id="Selenium.WebDriver" version="3.2.0" targetFramework="net40" /> <package id="Selenium.WebDriver" version="3.2.0" targetFramework="net40" />
@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using FluentAssertions; using FluentAssertions;
@@ -24,13 +25,60 @@ namespace NzbDrone.Common.Test.Http
[TestFixture(typeof(CurlHttpDispatcher))] [TestFixture(typeof(CurlHttpDispatcher))]
public class HttpClientFixture<TDispatcher> : TestBase<HttpClient> where TDispatcher : IHttpDispatcher public class HttpClientFixture<TDispatcher> : TestBase<HttpClient> where TDispatcher : IHttpDispatcher
{ {
private static string[] _httpBinHosts = new[] { "eu.httpbin.org", "httpbin.org" }; private string[] _httpBinHosts;
private static int _httpBinRandom; private int _httpBinSleep;
private int _httpBinRandom;
private string _httpBinHost; private string _httpBinHost;
private string _httpBinHost2;
[OneTimeSetUp]
public void FixtureSetUp()
{
var candidates = new[] { "eu.httpbin.org", /*"httpbin.org",*/ "www.httpbin.org" };
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
_httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray();
TestLogger.Info($"{candidates.Length} TestSites available.");
_httpBinSleep = _httpBinHosts.Count() < 2 ? 100 : 10;
}
private bool IsTestSiteAvailable(string site)
{
try
{
var req = WebRequest.Create($"http://{site}/get") as HttpWebRequest;
var res = req.GetResponse() as HttpWebResponse;
if (res.StatusCode != HttpStatusCode.OK) return false;
try
{
req = WebRequest.Create($"http://{site}/status/429") as HttpWebRequest;
res = req.GetResponse() as HttpWebResponse;
}
catch (WebException ex)
{
res = ex.Response as HttpWebResponse;
}
if (res == null || res.StatusCode != (HttpStatusCode)429) return false;
return true;
}
catch
{
return false;
}
}
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
if (!_httpBinHosts.Any())
{
Assert.Inconclusive("No TestSites available");
}
Mocker.GetMock<IPlatformInfo>().Setup(c => c.Version).Returns(new Version("1.0.0")); Mocker.GetMock<IPlatformInfo>().Setup(c => c.Version).Returns(new Version("1.0.0"));
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS"); Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0"); Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
@@ -50,6 +98,13 @@ namespace NzbDrone.Common.Test.Http
// Roundrobin over the two servers, to reduce the chance of hitting the ratelimiter. // Roundrobin over the two servers, to reduce the chance of hitting the ratelimiter.
_httpBinHost = _httpBinHosts[_httpBinRandom++ % _httpBinHosts.Length]; _httpBinHost = _httpBinHosts[_httpBinRandom++ % _httpBinHosts.Length];
_httpBinHost2 = _httpBinHosts[_httpBinRandom % _httpBinHosts.Length];
}
[TearDown]
public void TearDown()
{
Thread.Sleep(_httpBinSleep);
} }
[Test] [Test]
@@ -245,7 +300,12 @@ namespace NzbDrone.Common.Test.Http
public void GivenOldCookie() public void GivenOldCookie()
{ {
var oldRequest = new HttpRequest("http://eu.httpbin.org/get"); if (_httpBinHost == _httpBinHost2)
{
Assert.Inconclusive("Need both httpbin.org and eu.httpbin.org to run this test.");
}
var oldRequest = new HttpRequest($"http://{_httpBinHost2}/get");
oldRequest.Cookies["my"] = "cookie"; oldRequest.Cookies["my"] = "cookie";
var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.GetMock<IUserAgentBuilder>().Object, Mocker.Resolve<Logger>()); var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.GetMock<IUserAgentBuilder>().Object, Mocker.Resolve<Logger>());
@@ -262,7 +322,7 @@ namespace NzbDrone.Common.Test.Http
{ {
GivenOldCookie(); GivenOldCookie();
var request = new HttpRequest("http://eu.httpbin.org/get"); var request = new HttpRequest($"http://{_httpBinHost2}/get");
var response = Subject.Get<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
@@ -278,26 +338,103 @@ namespace NzbDrone.Common.Test.Http
{ {
GivenOldCookie(); GivenOldCookie();
var request = new HttpRequest("http://httpbin.org/get"); var request = new HttpRequest($"http://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie"); 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<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(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<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(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<HttpCookieResource>(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<HttpCookieResource>(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<HttpCookieResource>(requestClear);
responseClear.Resource.Cookies.Should().BeEmpty();
}
[Test] [Test]
public void should_not_store_response_cookie() public void should_not_store_response_cookie()
{ {
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get(requestSet); var responseSet = Subject.Get(requestSet);
var request = new HttpRequest($"http://{_httpBinHost}/get"); var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
var response = Subject.Get<HttpBinResource>(request); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
response.Resource.Headers.Should().NotContainKey("Cookie"); responseCookies.Resource.Cookies.Should().BeEmpty();
ExceptionVerification.IgnoreErrors(); ExceptionVerification.IgnoreErrors();
} }
@@ -307,19 +444,31 @@ namespace NzbDrone.Common.Test.Http
{ {
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet); var responseSet = Subject.Get(requestSet);
var request = new HttpRequest($"http://{_httpBinHost}/get"); var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
var response = Subject.Get<HttpBinResource>(request); var responseCookies = Subject.Get<HttpCookieResource>(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<HttpCookieResource>(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(); ExceptionVerification.IgnoreErrors();
} }
@@ -328,21 +477,129 @@ namespace NzbDrone.Common.Test.Http
public void should_overwrite_response_cookie() public void should_overwrite_response_cookie()
{ {
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie");
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
requestSet.Cookies["my"] = "oldcookie";
var responseSet = Subject.Get(requestSet); var responseSet = Subject.Get(requestSet);
var request = new HttpRequest($"http://{_httpBinHost}/get"); var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
var response = Subject.Get<HttpBinResource>(request); var responseCookies = Subject.Get<HttpCookieResource>(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<HttpCookieResource>(requestSet);
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(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<HttpCookieResource>(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<HttpCookieResource>(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<HttpCookieResource>(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<HttpCookieResource>(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<HttpCookieResource>(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<HttpCookieResource>(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(); ExceptionVerification.IgnoreErrors();
} }
@@ -454,4 +711,9 @@ namespace NzbDrone.Common.Test.Http
public string Url { get; set; } public string Url { get; set; }
public string Data { get; set; } public string Data { get; set; }
} }
public class HttpCookieResource
{
public Dictionary<string, string> Cookies { get; set; }
}
} }
@@ -48,16 +48,20 @@
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net40\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference> </Reference>
<Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> <Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath> <HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceProcess" /> <Reference Include="System.ServiceProcess" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
+1 -1
View File
@@ -2,6 +2,6 @@
<packages> <packages>
<package id="FluentAssertions" version="4.19.0" targetFramework="net40" /> <package id="FluentAssertions" version="4.19.0" targetFramework="net40" />
<package id="Moq" version="4.0.10827" targetFramework="net40" /> <package id="Moq" version="4.0.10827" targetFramework="net40" />
<package id="NLog" version="4.4.12" targetFramework="net40" /> <package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="NUnit" version="3.6.0" targetFramework="net40" /> <package id="NUnit" version="3.6.0" targetFramework="net40" />
</packages> </packages>
@@ -200,6 +200,11 @@ namespace NzbDrone.Common.Disk
throw new IOException(string.Format("Source and destination can't be the same {0}", source)); 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); File.Copy(source, destination, overwrite);
} }
@@ -219,6 +224,11 @@ namespace NzbDrone.Common.Disk
} }
RemoveReadOnly(source); RemoveReadOnly(source);
MoveFileInternal(source, destination);
}
protected virtual void MoveFileInternal(string source, string destination)
{
File.Move(source, destination); File.Move(source, destination);
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal; using System.Security.Principal;
using NLog; using NLog;
@@ -28,7 +28,15 @@ namespace NzbDrone.Common.EnvironmentInfo
public void Register() 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) if (OsInfo.IsWindows)
{ {
@@ -1,7 +1,10 @@
using System;
namespace NzbDrone.Common.EnvironmentInfo namespace NzbDrone.Common.EnvironmentInfo
{ {
public interface IRuntimeInfo public interface IRuntimeInfo
{ {
DateTime StartTime { get; }
bool IsUserInteractive { get; } bool IsUserInteractive { get; }
bool IsAdmin { get; } bool IsAdmin { get; }
bool IsWindowsService { get; } bool IsWindowsService { get; }
@@ -12,6 +12,7 @@ namespace NzbDrone.Common.EnvironmentInfo
public class RuntimeInfo : IRuntimeInfo public class RuntimeInfo : IRuntimeInfo
{ {
private readonly Logger _logger; private readonly Logger _logger;
private readonly DateTime _startTime = DateTime.UtcNow;
public RuntimeInfo(IServiceProvider serviceProvider, Logger logger) public RuntimeInfo(IServiceProvider serviceProvider, Logger logger)
{ {
@@ -37,6 +38,14 @@ namespace NzbDrone.Common.EnvironmentInfo
IsProduction = InternalIsProduction(); IsProduction = InternalIsProduction();
} }
public DateTime StartTime
{
get
{
return _startTime;
}
}
public static bool IsUserInteractive => Environment.UserInteractive; public static bool IsUserInteractive => Environment.UserInteractive;
bool IRuntimeInfo.IsUserInteractive => IsUserInteractive; bool IRuntimeInfo.IsUserInteractive => IsUserInteractive;
@@ -32,7 +32,19 @@ namespace NzbDrone.Common.Extensions
{ {
if (response == null || response.Content == null) return ex; if (response == null || response.Content == null) return ex;
var contentSample = response.Content.Substring(0, Math.Min(response.Content.Length, 512)); 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) if (response.Headers != null)
{ {
@@ -51,6 +51,34 @@ namespace NzbDrone.Common.Extensions
} }
} }
public static Dictionary<TKey, TItem> ToDictionaryIgnoreDuplicates<TItem, TKey>(this IEnumerable<TItem> src, Func<TItem, TKey> keySelector)
{
var result = new Dictionary<TKey, TItem>();
foreach (var item in src)
{
var key = keySelector(item);
if (!result.ContainsKey(key))
{
result[key] = item;
}
}
return result;
}
public static Dictionary<TKey, TValue> ToDictionaryIgnoreDuplicates<TItem, TKey, TValue>(this IEnumerable<TItem> src, Func<TItem, TKey> keySelector, Func<TItem, TValue> valueSelector)
{
var result = new Dictionary<TKey, TValue>();
foreach (var item in src)
{
var key = keySelector(item);
if (!result.ContainsKey(key))
{
result[key] = valueSelector(item);
}
}
return result;
}
public static void AddIfNotNull<TSource>(this List<TSource> source, TSource item) public static void AddIfNotNull<TSource>(this List<TSource> source, TSource item)
{ {
if (item == null) if (item == null)
@@ -81,4 +109,4 @@ namespace NzbDrone.Common.Extensions
return source.Select(predicate).ToList(); return source.Select(predicate).ToList();
} }
} }
} }
@@ -191,6 +191,24 @@ namespace NzbDrone.Common.Extensions
return directories; return directories;
} }
public static string GetAncestorPath(this string path, string ancestorName)
{
var parent = Path.GetDirectoryName(path);
while (parent != null)
{
var currentPath = parent;
parent = Path.GetDirectoryName(parent);
if (Path.GetFileName(currentPath) == ancestorName)
{
return currentPath;
}
}
return null;
}
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo) public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
{ {
return appFolderInfo.AppDataFolder; return appFolderInfo.AppDataFolder;
@@ -1,4 +1,5 @@
using System; using System;
using System.Globalization;
namespace NzbDrone.Common.Extensions namespace NzbDrone.Common.Extensions
{ {
@@ -6,7 +7,7 @@ namespace NzbDrone.Common.Extensions
{ {
public static int? ParseInt32(this string source) public static int? ParseInt32(this string source)
{ {
int result = 0; int result;
if (int.TryParse(source, out result)) if (int.TryParse(source, out result))
{ {
@@ -16,9 +17,9 @@ namespace NzbDrone.Common.Extensions
return null; return null;
} }
public static Nullable<long> ParseInt64(this string source) public static long? ParseInt64(this string source)
{ {
long result = 0; long result;
if (long.TryParse(source, out result)) if (long.TryParse(source, out result))
{ {
@@ -27,5 +28,17 @@ namespace NzbDrone.Common.Extensions
return null; 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;
}
} }
} }
@@ -1,4 +1,6 @@
using System; using System;
using System.IO;
using System.IO.Compression;
using System.Net; using System.Net;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@@ -24,11 +26,20 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url); var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
// Deflate is not a standard and could break depending on implementation. if (PlatformInfo.IsMono)
// we should just stick with the more compatible Gzip {
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net // On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
webRequest.AutomaticDecompression = DecompressionMethods.GZip; webRequest.AutomaticDecompression = DecompressionMethods.None;
webRequest.Headers.Add("Accept-Encoding", "gzip");
}
else
{
// Deflate is not a standard and could break depending on implementation.
// we should just stick with the more compatible Gzip
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
}
webRequest.Method = request.Method.ToString(); webRequest.Method = request.Method.ToString();
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent); webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
webRequest.KeepAlive = request.ConnectionKeepAlive; webRequest.KeepAlive = request.ConnectionKeepAlive;
@@ -101,11 +112,24 @@ namespace NzbDrone.Common.Http.Dispatchers
using (var responseStream = httpWebResponse.GetResponseStream()) using (var responseStream = httpWebResponse.GetResponseStream())
{ {
if (responseStream != null) if (responseStream != null && responseStream != Stream.Null)
{ {
try try
{ {
data = responseStream.ToBytes(); data = responseStream.ToBytes();
if (PlatformInfo.IsMono && httpWebResponse.ContentEncoding == "gzip")
{
using (var compressedStream = new MemoryStream(data))
using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress))
using (var decompressedStream = new MemoryStream())
{
gzip.CopyTo(decompressedStream);
data = decompressedStream.ToArray();
}
httpWebResponse.Headers.Remove("Content-Encoding");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
+81 -27
View File
@@ -52,7 +52,9 @@ namespace NzbDrone.Common.Http
public HttpResponse Execute(HttpRequest request) public HttpResponse Execute(HttpRequest request)
{ {
var response = ExecuteRequest(request); var cookieContainer = InitializeRequestCookies(request);
var response = ExecuteRequest(request, cookieContainer);
if (request.AllowAutoRedirect && response.HasHttpRedirect) if (request.AllowAutoRedirect && response.HasHttpRedirect)
{ {
@@ -71,7 +73,7 @@ namespace NzbDrone.Common.Http
throw new WebException($"Too many automatic redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError); throw new WebException($"Too many automatic redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError);
} }
response = ExecuteRequest(request); response = ExecuteRequest(request, cookieContainer);
} }
while (response.HasHttpRedirect); while (response.HasHttpRedirect);
} }
@@ -98,7 +100,7 @@ namespace NzbDrone.Common.Http
return response; return response;
} }
private HttpResponse ExecuteRequest(HttpRequest request) private HttpResponse ExecuteRequest(HttpRequest request, CookieContainer cookieContainer)
{ {
foreach (var interceptor in _requestInterceptors) foreach (var interceptor in _requestInterceptors)
{ {
@@ -114,11 +116,11 @@ namespace NzbDrone.Common.Http
var stopWatch = Stopwatch.StartNew(); var stopWatch = Stopwatch.StartNew();
var cookies = PrepareRequestCookies(request); PrepareRequestCookies(request, cookieContainer);
var response = _httpDispatcher.GetResponse(request, cookies); var response = _httpDispatcher.GetResponse(request, cookieContainer);
HandleResponseCookies(request, cookies); HandleResponseCookies(response, cookieContainer);
stopWatch.Stop(); stopWatch.Stop();
@@ -137,49 +139,91 @@ namespace NzbDrone.Common.Http
return response; return response;
} }
private CookieContainer PrepareRequestCookies(HttpRequest request) private CookieContainer InitializeRequestCookies(HttpRequest request)
{ {
lock (_cookieContainerCache) 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) if (request.Cookies.Count != 0)
{ {
foreach (var pair in request.Cookies) 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. cookie = new Cookie(pair.Key, "", "/")
// See https://gist.github.com/ta264/7822b1424f72e5b4c961 {
Expires = DateTime.Now.AddHours(1) 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); return sourceContainer;
var cookieContainer = new CookieContainer();
cookieContainer.Add(requestCookies);
return cookieContainer;
} }
} }
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; 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); foreach (var cookieHeader in cookieHeaders)
{
persistentCookieContainer.Add(cookies); try
{
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url);
}
}
}
} }
} }
@@ -223,6 +267,7 @@ namespace NzbDrone.Common.Http
public HttpResponse<T> Get<T>(HttpRequest request) where T : new() public HttpResponse<T> Get<T>(HttpRequest request) where T : new()
{ {
var response = Get(request); var response = Get(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response); return new HttpResponse<T>(response);
} }
@@ -241,7 +286,16 @@ namespace NzbDrone.Common.Http
public HttpResponse<T> Post<T>(HttpRequest request) where T : new() public HttpResponse<T> Post<T>(HttpRequest request) where T : new()
{ {
var response = Post(request); var response = Post(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response); return new HttpResponse<T>(response);
} }
private void CheckResponseContentType(HttpResponse response)
{
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
throw new UnexpectedHtmlContentException(response);
}
}
} }
} }
+9 -3
View File
@@ -7,13 +7,19 @@ namespace NzbDrone.Common.Http
public HttpRequest Request { get; private set; } public HttpRequest Request { get; private set; }
public HttpResponse Response { get; private set; } public HttpResponse Response { get; private set; }
public HttpException(HttpRequest request, HttpResponse response) public HttpException(HttpRequest request, HttpResponse response, string message)
: base(string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url)) : base(message)
{ {
Request = request; Request = request;
Response = response; Response = response;
} }
public HttpException(HttpRequest request, HttpResponse response)
: this(request, response, string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url))
{
}
public HttpException(HttpResponse response) public HttpException(HttpResponse response)
: this(response.Request, response) : this(response.Request, response)
{ {
@@ -30,4 +36,4 @@ namespace NzbDrone.Common.Http
return base.ToString(); return base.ToString();
} }
} }
} }
+12 -2
View File
@@ -13,8 +13,10 @@ namespace NzbDrone.Common.Http
Url = new HttpUri(url); Url = new HttpUri(url);
Headers = new HttpHeader(); Headers = new HttpHeader();
AllowAutoRedirect = true; AllowAutoRedirect = true;
StoreRequestCookie = true;
Cookies = new Dictionary<string, string>(); Cookies = new Dictionary<string, string>();
if (!RuntimeInfo.IsProduction) if (!RuntimeInfo.IsProduction)
{ {
AllowAutoRedirect = false; AllowAutoRedirect = false;
@@ -37,6 +39,7 @@ namespace NzbDrone.Common.Http
public bool ConnectionKeepAlive { get; set; } public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; } public bool LogResponseContent { get; set; }
public Dictionary<string, string> Cookies { get; private set; } public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; } public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; } public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; } public TimeSpan RateLimit { get; set; }
@@ -76,5 +79,12 @@ namespace NzbDrone.Common.Http
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType); var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
ContentData = encoding.GetBytes(data); 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);
}
} }
} }
+10 -8
View File
@@ -55,20 +55,22 @@ namespace NzbDrone.Common.Http
StatusCode == HttpStatusCode.MovedPermanently || StatusCode == HttpStatusCode.MovedPermanently ||
StatusCode == HttpStatusCode.Found; StatusCode == HttpStatusCode.Found;
public string[] GetCookieHeaders()
{
return Headers.GetValues("Set-Cookie") ?? new string[0];
}
public Dictionary<string, string> GetCookies() public Dictionary<string, string> GetCookies()
{ {
var result = new Dictionary<string, string>(); var result = new Dictionary<string, string>();
var setCookieHeaders = Headers.GetValues("Set-Cookie"); var setCookieHeaders = GetCookieHeaders();
if (setCookieHeaders != null) foreach (var cookie in setCookieHeaders)
{ {
foreach (var cookie in setCookieHeaders) var match = RegexSetCookie.Match(cookie);
if (match.Success)
{ {
var match = RegexSetCookie.Match(cookie); result[match.Groups[1].Value] = match.Groups[2].Value;
if (match.Success)
{
result[match.Groups[1].Value] = match.Groups[2].Value;
}
} }
} }
+1 -1
View File
@@ -135,7 +135,7 @@ namespace NzbDrone.Common.Http
return new HttpUri(Scheme, Host, Port, CombinePath(Path, path), Query, Fragment); return new HttpUri(Scheme, Host, Port, CombinePath(Path, path), Query, Fragment);
} }
private static string CombinePath(string basePath, string relativePath) public static string CombinePath(string basePath, string relativePath)
{ {
if (relativePath.IsNullOrWhiteSpace()) if (relativePath.IsNullOrWhiteSpace())
{ {
@@ -0,0 +1,13 @@
using System;
namespace NzbDrone.Common.Http
{
public class UnexpectedHtmlContentException : HttpException
{
public UnexpectedHtmlContentException(HttpResponse response)
: base(response.Request, response, $"Site responded with browser content instead of api data. This disruption may be temporary, please try again later. [{response.Request.Url}]")
{
}
}
}
@@ -51,7 +51,7 @@ namespace NzbDrone.Common.Instrumentation.Extensions
return logBuilder.LoggerName(logEvent.LoggerName) return logBuilder.LoggerName(logEvent.LoggerName)
.TimeStamp(logEvent.TimeStamp) .TimeStamp(logEvent.TimeStamp)
.Message(logEvent.Message, logEvent.Parameters) .Message(logEvent.Message, logEvent.Parameters)
.Properties((Dictionary<object, object>)logEvent.Properties) .Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
.Exception(logEvent.Exception); .Exception(logEvent.Exception);
} }
} }
@@ -31,7 +31,7 @@ namespace NzbDrone.Common.Instrumentation
if (exception is NullReferenceException && if (exception is NullReferenceException &&
exception.ToString().Contains("Microsoft.AspNet.SignalR.Transports.TransportHeartbeat.ProcessServerCommand")) exception.ToString().Contains("Microsoft.AspNet.SignalR.Transports.TransportHeartbeat.ProcessServerCommand"))
{ {
Logger.Warn("SignalR Heartbeat interupted"); Logger.Warn("SignalR Heartbeat interrupted");
return; return;
} }
@@ -49,4 +49,4 @@ namespace NzbDrone.Common.Instrumentation
Logger.Fatal(exception, "EPIC FAIL."); Logger.Fatal(exception, "EPIC FAIL.");
} }
} }
} }
+6 -1
View File
@@ -44,7 +44,7 @@
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net40\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference> </Reference>
<Reference Include="Org.Mentalis, Version=1.0.0.1, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="Org.Mentalis, Version=1.0.0.1, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll</HintPath> <HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll</HintPath>
@@ -60,11 +60,14 @@
<Reference Include="System.Configuration.Install" /> <Reference Include="System.Configuration.Install" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceProcess" /> <Reference Include="System.ServiceProcess" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
<Reference Include="ICSharpCode.SharpZipLib"> <Reference Include="ICSharpCode.SharpZipLib">
<HintPath>..\packages\ICSharpCode.SharpZipLib.Patched.0.86.5\lib\net20\ICSharpCode.SharpZipLib.dll</HintPath> <HintPath>..\packages\ICSharpCode.SharpZipLib.Patched.0.86.5\lib\net20\ICSharpCode.SharpZipLib.dll</HintPath>
</Reference> </Reference>
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
</ItemGroup> </ItemGroup>
@@ -174,6 +177,7 @@
<Compile Include="Http\HttpRequestBuilderFactory.cs" /> <Compile Include="Http\HttpRequestBuilderFactory.cs" />
<Compile Include="Http\Proxy\ProxyType.cs" /> <Compile Include="Http\Proxy\ProxyType.cs" />
<Compile Include="Http\TlsFailureException.cs" /> <Compile Include="Http\TlsFailureException.cs" />
<Compile Include="Http\UnexpectedHtmlContentException.cs" />
<Compile Include="Http\TooManyRequestsException.cs" /> <Compile Include="Http\TooManyRequestsException.cs" />
<Compile Include="Extensions\IEnumerableExtensions.cs" /> <Compile Include="Extensions\IEnumerableExtensions.cs" />
<Compile Include="Http\UserAgentBuilder.cs" /> <Compile Include="Http\UserAgentBuilder.cs" />
@@ -212,6 +216,7 @@
<Compile Include="Serializer\IntConverter.cs" /> <Compile Include="Serializer\IntConverter.cs" />
<Compile Include="Serializer\Json.cs" /> <Compile Include="Serializer\Json.cs" />
<Compile Include="Serializer\JsonVisitor.cs" /> <Compile Include="Serializer\JsonVisitor.cs" />
<Compile Include="Serializer\UnderscoreStringEnumConverter.cs" />
<Compile Include="ServiceFactory.cs" /> <Compile Include="ServiceFactory.cs" />
<Compile Include="ServiceProvider.cs" /> <Compile Include="ServiceProvider.cs" />
<Compile Include="Extensions\StringExtensions.cs" /> <Compile Include="Extensions\StringExtensions.cs" />
+60 -10
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Reflection;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
@@ -14,13 +15,13 @@ namespace NzbDrone.Common.Serializer
static Json() static Json()
{ {
SerializerSetting = new JsonSerializerSettings SerializerSetting = new JsonSerializerSettings
{ {
DateTimeZoneHandling = DateTimeZoneHandling.Utc, DateTimeZoneHandling = DateTimeZoneHandling.Utc,
NullValueHandling = NullValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.Indented, Formatting = Formatting.Indented,
DefaultValueHandling = DefaultValueHandling.Include, DefaultValueHandling = DefaultValueHandling.Include,
ContractResolver = new CamelCasePropertyNamesContractResolver() ContractResolver = new CamelCasePropertyNamesContractResolver()
}; };
SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true }); SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true });
@@ -34,12 +35,61 @@ namespace NzbDrone.Common.Serializer
public static T Deserialize<T>(string json) where T : new() public static T Deserialize<T>(string json) where T : new()
{ {
return JsonConvert.DeserializeObject<T>(json, SerializerSetting); try
{
return JsonConvert.DeserializeObject<T>(json, SerializerSetting);
}
catch (JsonReaderException ex)
{
throw DetailedJsonReaderException(ex, json);
}
} }
public static object Deserialize(string json, Type type) public static object Deserialize(string json, Type type)
{ {
return JsonConvert.DeserializeObject(json, type, SerializerSetting); try
{
return JsonConvert.DeserializeObject(json, type, SerializerSetting);
}
catch (JsonReaderException ex)
{
throw DetailedJsonReaderException(ex, json);
}
}
private static JsonReaderException DetailedJsonReaderException(JsonReaderException ex, string json)
{
var lineNumber = ex.LineNumber == 0 ? 0 : (ex.LineNumber - 1);
var linePosition = ex.LinePosition;
var lines = json.Split('\n');
if (lineNumber >= 0 && lineNumber < lines.Length &&
linePosition >= 0 && linePosition < lines[lineNumber].Length)
{
var line = lines[lineNumber];
var start = Math.Max(0, linePosition - 20);
var end = Math.Min(line.Length, linePosition + 20);
var snippetBefore = line.Substring(start, linePosition - start);
var snippetAfter = line.Substring(linePosition, end - linePosition);
var message = ex.Message + " (Json snippet '" + snippetBefore + "<--error-->" + snippetAfter + "')";
// Not risking updating JSON.net from 9.x to 10.x just to get this as public ctor.
var ctor = typeof(JsonReaderException).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(string), typeof(Exception), typeof(string), typeof(int), typeof(int) }, null);
if (ctor != null)
{
return (JsonReaderException)ctor.Invoke(new object[] { message, ex, ex.Path, ex.LineNumber, linePosition });
}
// JSON.net 10.x ctor in case we update later.
ctor = typeof(JsonReaderException).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(string), typeof(string), typeof(int), typeof(int), typeof(Exception) }, null);
if (ctor != null)
{
return (JsonReaderException)ctor.Invoke(new object[] { message, ex.Path, ex.LineNumber, linePosition, ex });
}
}
return ex;
} }
public static bool TryDeserialize<T>(string json, out T result) where T : new() public static bool TryDeserialize<T>(string json, out T result) where T : new()
@@ -78,4 +128,4 @@ namespace NzbDrone.Common.Serializer
Serialize(model, new StreamWriter(outputStream)); Serialize(model, new StreamWriter(outputStream));
} }
} }
} }
@@ -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);
}
}
}
+1 -1
View File
@@ -3,6 +3,6 @@
<package id="DotNet4.SocksProxy" version="1.3.4.0" targetFramework="net40" /> <package id="DotNet4.SocksProxy" version="1.3.4.0" targetFramework="net40" />
<package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net40" /> <package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net40" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" />
<package id="NLog" version="4.4.12" targetFramework="net40" /> <package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="SharpRaven" version="2.2.0" targetFramework="net40" /> <package id="SharpRaven" version="2.2.0" targetFramework="net40" />
</packages> </packages>
+9 -1
View File
@@ -25,7 +25,15 @@ namespace NzbDrone.Console
try try
{ {
var startupArgs = new StartupContext(args); 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()); Bootstrap.Start(startupArgs, new ConsoleAlerts());
} }
catch (SonarrStartupException ex) catch (SonarrStartupException ex)
+8 -1
View File
@@ -66,6 +66,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.Owin, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <Reference Include="Microsoft.Owin, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.Owin.2.1.0\lib\net40\Microsoft.Owin.dll</HintPath> <HintPath>..\packages\Microsoft.Owin.2.1.0\lib\net40\Microsoft.Owin.dll</HintPath>
@@ -79,13 +80,19 @@
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net40\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="Owin"> <Reference Include="Owin">
<HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath>
</Reference> </Reference>
<Reference Include="System.Data" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs"> <Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs">
+1 -1
View File
@@ -3,6 +3,6 @@
<package id="Microsoft.Owin" version="2.1.0" targetFramework="net40" /> <package id="Microsoft.Owin" version="2.1.0" targetFramework="net40" />
<package id="Microsoft.Owin.Hosting" version="2.1.0" targetFramework="net40" /> <package id="Microsoft.Owin.Hosting" version="2.1.0" targetFramework="net40" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" />
<package id="NLog" version="4.4.12" targetFramework="net40" /> <package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="Owin" version="1.0" targetFramework="net40" /> <package id="Owin" version="1.0" targetFramework="net40" />
</packages> </packages>
@@ -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<Core.Datastore.Converters.BooleanIntConverter>
{
[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<ConversionException>(() => Subject.FromDB(context));
}
}
}
@@ -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<CommandConverter>
{
[Test]
public void should_return_json_string_when_saving_boolean_to_db()
{
var command = new RefreshSeriesCommand();
Subject.ToDB(command).Should().BeOfType<string>();
}
[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<IDataRecord>();
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<RefreshSeriesCommand>();
}
[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);
}
}
}
@@ -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<DoubleConverter>
{
[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);
}
}
}
@@ -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<Core.Datastore.Converters.EnumIntConverter>
{
[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<MemberInfo>();
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);
}
}
}
@@ -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<GuidConverter>
{
[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);
}
}
}
@@ -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<Int32Converter>
{
[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);
}
}
}
@@ -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<OsPathConverter>
{
[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);
}
}
}
@@ -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<QualityIntConverter>
{
[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<InvalidOperationException>(() => 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);
}
}
}
@@ -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<TimeSpanConverter>
{
[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);
}
}
}
@@ -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<UtcConverter>
{
[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);
}
}
}
@@ -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<update_animetosho_url>
{
[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<IndexerDefinition90>("SELECT * FROM Indexers");
items.Should().HaveCount(1);
items.First().Settings.ToObject<NewznabSettings121>().BaseUrl.Should().Be(baseUrl.Replace("animetosho", "feed.animetosho"));
}
}
}
@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.History;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.DecisionEngineTests
{
[TestFixture]
public class AlreadyImportedSpecificationFixture : CoreTest<AlreadyImportedSpecification>
{
private const int FIRST_EPISODE_ID = 1;
private const string TITLE = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
private Series _series;
private QualityModel _hdtv720p;
private QualityModel _hdtv1080p;
private RemoteEpisode _remoteEpisode;
private List<History.History> _history;
[SetUp]
public void Setup()
{
var singleEpisodeList = new List<Episode>
{
new Episode
{
Id = FIRST_EPISODE_ID,
SeasonNumber = 12,
EpisodeNumber = 3,
EpisodeFileId = 1
}
};
_series = Builder<Series>.CreateNew()
.Build();
_hdtv720p = new QualityModel(Quality.HDTV720p, new Revision(version: 1));
_hdtv1080p = new QualityModel(Quality.HDTV1080p, new Revision(version: 1));
_remoteEpisode = new RemoteEpisode
{
Series = _series,
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = _hdtv720p },
Episodes = singleEpisodeList,
Release = Builder<ReleaseInfo>.CreateNew()
.Build()
};
_history = new List<History.History>();
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.EnableCompletedDownloadHandling)
.Returns(true);
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByEpisodeId(It.IsAny<int>()))
.Returns(_history);
}
private void GivenCdhDisabled()
{
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.EnableCompletedDownloadHandling)
.Returns(false);
}
private void GivenHistoryItem(string downloadId, string sourceTitle, QualityModel quality, HistoryEventType eventType)
{
_history.Add(new History.History
{
DownloadId = downloadId,
SourceTitle = sourceTitle,
Quality = quality,
Date = DateTime.UtcNow,
EventType = eventType
});
}
[Test]
public void should_be_accepted_if_CDH_is_disabled()
{
GivenCdhDisabled();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_episode_does_not_have_a_file()
{
_remoteEpisode.Episodes.First().EpisodeFileId = 0;
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_episode_does_not_have_grabbed_event()
{
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_episode_does_not_have_imported_event()
{
GivenHistoryItem(Guid.NewGuid().ToString().ToUpper(), TITLE, _hdtv720p, HistoryEventType.Grabbed);
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_grabbed_and_imported_quality_is_the_same()
{
var downloadId = Guid.NewGuid().ToString().ToUpper();
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.Grabbed);
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.DownloadFolderImported);
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_rejected_if_grabbed_download_id_matches_release_torrent_hash()
{
var downloadId = Guid.NewGuid().ToString().ToUpper();
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.Grabbed);
GivenHistoryItem(downloadId, TITLE, _hdtv1080p, HistoryEventType.DownloadFolderImported);
_remoteEpisode.Release = Builder<TorrentInfo>.CreateNew()
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
.With(t => t.InfoHash = downloadId)
.Build();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
[Test]
public void should_be_rejected_if_release_title_matches_grabbed_event_source_title()
{
var downloadId = Guid.NewGuid().ToString().ToUpper();
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.Grabbed);
GivenHistoryItem(downloadId, TITLE, _hdtv1080p, HistoryEventType.DownloadFolderImported);
_remoteEpisode.Release = Builder<TorrentInfo>.CreateNew()
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
.With(t => t.InfoHash = downloadId)
.Build();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
}
}
@@ -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<MaximumSizeSpecification>
{
private RemoteEpisode _remoteEpisode;
[SetUp]
public void Setup()
{
_remoteEpisode = new RemoteEpisode() { Release = new ReleaseInfo() };
}
private void WithMaximumSize(int size)
{
Mocker.GetMock<IConfigService>().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();
}
}
}
@@ -29,6 +29,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR" Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR"
} }
}; };
Mocker.SetConstant<ITermMatcher>(Mocker.Resolve<TermMatcher>());
} }
private void GivenRestictions(string required, string ignored) private void GivenRestictions(string required, string ignored)
@@ -123,5 +125,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); 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);
}
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
@@ -16,7 +16,7 @@ using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.DecisionEngineTests namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
{ {
[TestFixture] [TestFixture]
public class HistorySpecificationFixture : CoreTest<HistorySpecification> public class HistorySpecificationFixture : CoreTest<HistorySpecification>
@@ -1,12 +1,11 @@
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications.Search; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.TorrentRss; using NzbDrone.Core.Indexers.TorrentRss;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@@ -137,6 +137,7 @@ namespace NzbDrone.Core.Test.DiskSpace
[TestCase("/var/lib/kubelet")] [TestCase("/var/lib/kubelet")]
[TestCase("/var/lib/docker")] [TestCase("/var/lib/docker")]
[TestCase("/some/place/docker/aufs")] [TestCase("/some/place/docker/aufs")]
[TestCase("/etc/network")]
public void should_not_check_diskspace_for_irrelevant_mounts(string path) public void should_not_check_diskspace_for_irrelevant_mounts(string path)
{ {
var mount = new Mock<IMount>(); var mount = new Mock<IMount>();
@@ -212,7 +212,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary))); decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions); Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.Add(It.IsAny<DownloadDecision>(), It.IsAny<PendingReleaseReason>()), Times.Never()); Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Never());
} }
[Test] [Test]
@@ -226,7 +226,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary))); decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions); Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.Add(It.IsAny<DownloadDecision>(), It.IsAny<PendingReleaseReason>()), Times.Exactly(2)); Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Once());
} }
[Test] [Test]
@@ -1,8 +1,9 @@
using System; using System;
using System.Linq; using System.Linq;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -16,6 +17,10 @@ namespace NzbDrone.Core.Test.Download
public void SetUp() public void SetUp()
{ {
_epoch = DateTime.UtcNow; _epoch = DateTime.UtcNow;
Mocker.GetMock<IRuntimeInfo>()
.SetupGet(v => v.StartTime)
.Returns(_epoch - TimeSpan.FromHours(1));
} }
private DownloadClientStatus WithStatus(DownloadClientStatus status) private DownloadClientStatus WithStatus(DownloadClientStatus status)
@@ -290,6 +290,24 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
item.CanBeRemoved.Should().Be(canBeRemoved); item.CanBeRemoved.Should().Be(canBeRemoved);
} }
[Test]
public void GetItems_should_ignore_items_without_hash()
{
_downloading.Hash = null;
GivenTorrents(new List<DelugeTorrent>
{
_downloading,
_queued
});
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().Status.Should().Be(DownloadItemStatus.Queued);
}
[Test] [Test]
public void should_return_status_with_outputdirs() public void should_return_status_with_outputdirs()
{ {
@@ -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<DownloadStationTask>(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<DownloadStationTask>(task);
item.Status.Should().Be(DownloadStationTaskStatus.Unknown);
}
}
}
@@ -73,6 +73,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string> Transfer = new Dictionary<string, string>
{ {
{ "size_downloaded", "0"}, { "size_downloaded", "0"},
{ "size_uploaded", "0"},
{ "speed_download", "0" } { "speed_download", "0" }
} }
} }
@@ -96,6 +97,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string> Transfer = new Dictionary<string, string>
{ {
{ "size_downloaded", "1000"}, { "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" } { "speed_download", "0" }
}, },
} }
@@ -119,6 +121,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string> Transfer = new Dictionary<string, string>
{ {
{ "size_downloaded", "1000"}, { "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" } { "speed_download", "0" }
} }
} }
@@ -142,6 +145,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string> Transfer = new Dictionary<string, string>
{ {
{ "size_downloaded", "100"}, { "size_downloaded", "100"},
{ "size_uploaded", "10"},
{ "speed_download", "50" } { "speed_download", "50" }
} }
} }
@@ -165,6 +169,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string> Transfer = new Dictionary<string, string>
{ {
{ "size_downloaded", "10"}, { "size_downloaded", "10"},
{ "size_uploaded", "1"},
{ "speed_download", "0" } { "speed_download", "0" }
} }
} }
@@ -188,6 +193,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string> Transfer = new Dictionary<string, string>
{ {
{ "size_downloaded", "1000"}, { "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" } { "speed_download", "0" }
} }
} }
@@ -211,6 +217,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string> Transfer = new Dictionary<string, string>
{ {
{ "size_downloaded", "1000"}, { "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" } { "speed_download", "0" }
} }
} }
@@ -234,6 +241,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string> Transfer = new Dictionary<string, string>
{ {
{ "size_downloaded", "1000"}, { "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" } { "speed_download", "0" }
} }
} }
@@ -257,6 +265,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string> Transfer = new Dictionary<string, string>
{ {
{ "size_downloaded", "1000"}, { "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" } { "speed_download", "0" }
} }
} }
@@ -605,9 +614,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)] [TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)]
[TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.CaptchaNeeded, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)] [TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)]
[TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed)] [TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed)]
[TestCase(DownloadStationTaskStatus.FilehostingWaiting, DownloadItemStatus.Queued)]
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)] [TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)]
[TestCase(DownloadStationTaskStatus.Unknown, DownloadItemStatus.Queued)]
public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus) public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus)
{ {
GivenSerialNumber(); GivenSerialNumber();
@@ -414,8 +414,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)] [TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)]
[TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.CaptchaNeeded, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)] [TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)]
[TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed)]
[TestCase(DownloadStationTaskStatus.FilehostingWaiting, DownloadItemStatus.Queued)]
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)] [TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)]
[TestCase(DownloadStationTaskStatus.Unknown, DownloadItemStatus.Queued)]
public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus) public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus)
{ {
GivenSerialNumber(); GivenSerialNumber();
@@ -162,6 +162,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[TestCase("uploading")] [TestCase("uploading")]
[TestCase("stalledUP")] [TestCase("stalledUP")]
[TestCase("checkingUP")] [TestCase("checkingUP")]
[TestCase("forcedUP")]
public void completed_item_should_have_required_properties(string state) public void completed_item_should_have_required_properties(string state)
{ {
var torrent = new QBittorrentTorrent var torrent = new QBittorrentTorrent
@@ -494,5 +495,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.Category.Should().Be(category); item.Category.Should().Be(category);
} }
[Test]
public void should_handle_eta_biginteger()
{
// Let this stand as a lesson to never write temporary unit tests on your dev machine and claim it works.
// Commit the tests and let it run with the official build on the official build agents.
// (Also don't replace library versions in your build script)
var json = "{ \"eta\": 18446744073709335000 }";
var torrent = Newtonsoft.Json.JsonConvert.DeserializeObject<QBittorrentTorrent>(json);
torrent.Eta.ToString().Should().Be("18446744073709335000");
}
} }
} }
@@ -205,18 +205,26 @@ namespace NzbDrone.Core.Test.Download
} }
[Test] [Test]
public void should_not_attempt_download_if_client_is_disabled() public void should_attempt_download_even_if_client_is_disabled()
{ {
WithUsenetClient(); var mockUsenet = WithUsenetClient();
Mocker.GetMock<IDownloadClientStatusService>() Mocker.GetMock<IDownloadClientStatusService>()
.Setup(v => v.IsDisabled(It.IsAny<int>())) .Setup(v => v.GetBlockedProviders())
.Returns(true); .Returns(new List<DownloadClientStatus>
{
new DownloadClientStatus
{
ProviderId = _downloadClients.First().Definition.Id,
DisabledTill = DateTime.UtcNow.AddHours(3)
}
});
Assert.Throws<DownloadClientUnavailableException>(() => Subject.DownloadReport(_parseResult)); Subject.DownloadReport(_parseResult);
Mocker.GetMock<IDownloadClient>().Verify(c => c.Download(It.IsAny<RemoteEpisode>()), Times.Never()); Mocker.GetMock<IDownloadClientStatusService>().Verify(c => c.GetBlockedProviders(), Times.Never());
VerifyEventNotPublished<EpisodeGrabbedEvent>(); mockUsenet.Verify(c => c.Download(It.IsAny<RemoteEpisode>()), Times.Once());
VerifyEventPublished<EpisodeGrabbedEvent>();
} }
[Test] [Test]
@@ -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<NzbValidationService>
{
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<InvalidNzbException>(() => Subject.Validate(filename, fileContent));
}
[Test]
public void should_throw_when_no_files()
{
var filename = "NoFiles";
var fileContent = GivenNzbFile(filename);
Assert.Throws<InvalidNzbException>(() => Subject.Validate(filename, fileContent));
}
[Test]
public void should_validate_nzb()
{
var filename = "ValidNzb";
var fileContent = GivenNzbFile(filename);
Subject.Validate(filename, fileContent);
}
}
}

Some files were not shown because too many files have changed in this diff Show More