From 08496c82afe3cc6b30c1529debd45da82f3df52d Mon Sep 17 00:00:00 2001 From: ta264 Date: Wed, 6 May 2020 21:14:11 +0100 Subject: [PATCH] New: Readarr 0.1 --- Logo/1024.png | Bin 52098 -> 39632 bytes Logo/128.png | Bin 5717 -> 4813 bytes Logo/16.png | Bin 566 -> 475 bytes Logo/256.png | Bin 13833 -> 11658 bytes Logo/32.png | Bin 1365 -> 1285 bytes Logo/400.png | Bin 24413 -> 19113 bytes Logo/48.png | Bin 2203 -> 1822 bytes Logo/512.png | Bin 30123 -> 23550 bytes Logo/64.png | Bin 2780 -> 2013 bytes Logo/800.png | Bin 53692 -> 38588 bytes Logo/Readarr.svg | 44 +- README.md | 4 +- azure-pipelines.yml | 42 +- debian/control | 2 +- debian/copyright | 2 +- .../src/Activity/Blacklist/BlacklistRow.js | 5 +- .../src/Activity/History/HistoryConnector.js | 6 +- frontend/src/Activity/History/HistoryRow.js | 26 +- frontend/src/Activity/Queue/Queue.js | 8 +- frontend/src/Activity/Queue/QueueConnector.js | 6 +- frontend/src/Activity/Queue/QueueRow.js | 10 +- .../ArtistMonitoringOptionsPopoverContent.js | 26 +- frontend/src/Album/AlbumSearchCell.js | 8 +- .../src/Album/AlbumSearchCellConnector.js | 8 +- frontend/src/Album/AlbumTitleLink.js | 6 +- .../DeleteAlbumModalContentConnector.js | 8 +- frontend/src/Album/Details/AlbumDetails.css | 26 +- frontend/src/Album/Details/AlbumDetails.js | 264 +- .../Album/Details/AlbumDetailsConnector.js | 51 +- .../src/Album/Details/AlbumDetailsLinks.js | 15 - .../src/Album/Details/AlbumDetailsMedium.js | 210 -- .../Details/AlbumDetailsMediumConnector.js | 65 - .../Details/AlbumDetailsPageConnector.js | 24 +- .../src/Album/Details/TrackActionsCell.css | 6 - .../src/Album/Details/TrackActionsCell.js | 109 - frontend/src/Album/Details/TrackRow.css | 30 - frontend/src/Album/Details/TrackRow.js | 166 - .../src/Album/Details/TrackRowConnector.js | 23 - frontend/src/Album/Edit/EditAlbumModal.js | 25 - .../src/Album/Edit/EditAlbumModalConnector.js | 39 - .../src/Album/Edit/EditAlbumModalContent.js | 133 - .../Edit/EditAlbumModalContentConnector.js | 98 - frontend/src/Album/EpisodeStatusConnector.js | 2 +- .../Search/AlbumInteractiveSearchModal.js | 6 +- .../AlbumInteractiveSearchModalContent.js | 8 +- frontend/src/AlbumStudio/AlbumStudio.js | 6 +- frontend/src/AlbumStudio/AlbumStudioAlbum.js | 10 - frontend/src/AlbumStudio/AlbumStudioFooter.js | 6 +- frontend/src/AlbumStudio/AlbumStudioRow.js | 12 +- .../AlbumStudio/AlbumStudioRowConnector.js | 16 +- frontend/src/App/AppRoutes.js | 4 +- frontend/src/Artist/ArtistLogo.js | 160 - frontend/src/Artist/ArtistNameLink.js | 6 +- frontend/src/Artist/ArtistPoster.js | 2 +- .../DeleteArtistModalContentConnector.js | 4 +- frontend/src/Artist/Details/AlbumRow.js | 68 +- .../src/Artist/Details/AlbumRowConnector.js | 1 - frontend/src/Artist/Details/ArtistDetails.css | 30 +- frontend/src/Artist/Details/ArtistDetails.js | 245 +- .../Artist/Details/ArtistDetailsConnector.js | 96 +- .../src/Artist/Details/ArtistDetailsLinks.js | 15 - .../Details/ArtistDetailsPageConnector.js | 22 +- .../src/Artist/Details/ArtistDetailsSeason.js | 206 +- .../Details/ArtistDetailsSeasonConnector.js | 8 +- .../Details/AuthorDetailsSeries.css} | 45 +- .../src/Artist/Details/AuthorDetailsSeries.js | 205 ++ .../Details/AuthorDetailsSeriesConnector.js | 121 + .../src/Artist/Edit/EditArtistModalContent.js | 15 +- .../Edit/EditArtistModalContentConnector.js | 4 +- frontend/src/Artist/Editor/ArtistEditor.js | 18 +- .../src/Artist/Editor/ArtistEditorFooter.js | 8 +- frontend/src/Artist/Editor/ArtistEditorRow.js | 18 +- .../RetagArtistModalContentConnector.js | 10 +- .../DeleteArtistModalContentConnector.js | 8 +- .../OrganizeArtistModalContentConnector.js | 10 +- .../Editor/Tags/TagsModalContentConnector.js | 6 +- ...or.js => ArtistHistoryContentConnector.js} | 37 +- .../src/Artist/History/ArtistHistoryModal.js | 6 +- .../History/ArtistHistoryModalContent.js | 94 +- .../src/Artist/History/ArtistHistoryTable.js | 21 + .../History/ArtistHistoryTableContent.js | 113 + .../src/Artist/Index/ArtistIndexFooter.js | 12 +- .../Artist/Index/ArtistIndexItemConnector.js | 8 +- .../Index/Banners/ArtistIndexBanner.css | 2 +- .../Artist/Index/Banners/ArtistIndexBanner.js | 10 +- .../Index/Banners/ArtistIndexBanners.js | 2 +- .../Index/Overview/ArtistIndexOverview.js | 10 +- .../Index/Overview/ArtistIndexOverviews.js | 2 +- .../Index/Posters/ArtistIndexPoster.css | 2 +- .../Artist/Index/Posters/ArtistIndexPoster.js | 11 +- .../Index/Posters/ArtistIndexPosters.js | 6 +- .../Index/Table/ArtistIndexActionsCell.js | 4 +- .../src/Artist/Index/Table/ArtistIndexRow.js | 16 +- .../Artist/Index/Table/ArtistIndexTable.js | 2 +- frontend/src/Artist/NoArtist.js | 6 +- .../Search/ArtistInteractiveSearchModal.js | 33 - .../ArtistInteractiveSearchModalConnector.js | 15 - .../ArtistInteractiveSearchModalContent.js | 45 - frontend/src/Calendar/Agenda/Agenda.js | 2 +- frontend/src/Calendar/Agenda/AgendaEvent.js | 8 +- frontend/src/Calendar/CalendarConnector.js | 4 +- frontend/src/Calendar/CalendarPage.js | 10 +- .../src/Calendar/CalendarPageConnector.js | 12 +- frontend/src/Calendar/Day/CalendarDay.js | 2 +- frontend/src/Calendar/Events/CalendarEvent.js | 8 +- frontend/src/Components/Form/PlaylistInput.js | 8 +- frontend/src/Components/HeartRating.js | 2 +- .../Page/Header/ArtistSearchInput.js | 2 +- .../Page/Header/ArtistSearchInputConnector.js | 8 +- .../src/Components/Page/Header/PageHeader.css | 6 +- .../Components/Page/Sidebar/PageSidebar.js | 2 +- .../Page/Sidebar/PageSidebarItem.css | 6 +- frontend/src/Components/StarRating.js | 4 +- frontend/src/Components/Table/VirtualTable.js | 1 + .../Images/Icons/android-chrome-192x192.png | Bin 20949 -> 20595 bytes .../Images/Icons/android-chrome-512x512.png | Bin 71539 -> 62414 bytes .../Content/Images/Icons/apple-touch-icon.png | Bin 19865 -> 19102 bytes .../Content/Images/Icons/browserconfig.xml | 4 +- .../Content/Images/Icons/favicon-16x16.png | Bin 685 -> 1124 bytes .../Content/Images/Icons/favicon-32x32.png | Bin 1953 -> 2526 bytes .../Images/Icons/favicon-debug-16x16.png | Bin 671 -> 1150 bytes .../Images/Icons/favicon-debug-32x32.png | Bin 1944 -> 2548 bytes .../Content/Images/Icons/favicon-debug.ico | Bin 15086 -> 15086 bytes frontend/src/Content/Images/Icons/favicon.ico | Bin 15086 -> 15086 bytes .../Content/Images/Icons/mstile-144x144.png | Bin 15167 -> 14922 bytes .../Content/Images/Icons/mstile-150x150.png | Bin 14626 -> 13845 bytes .../Content/Images/Icons/mstile-310x150.png | Bin 16149 -> 14645 bytes .../Content/Images/Icons/mstile-310x310.png | Bin 35066 -> 32047 bytes .../src/Content/Images/Icons/mstile-70x70.png | Bin 9313 -> 9454 bytes .../Images/Icons/safari-pinned-tab.svg | 65 +- frontend/src/Content/Images/logo.svg | 24 +- .../src/Content/Images/poster-dark-square.png | Bin 1699 -> 1121 bytes frontend/src/Content/Images/poster-dark.png | Bin 1622 -> 1074 bytes .../Album/SelectAlbumModalContent.js | 13 +- .../Album/SelectAlbumModalContentConnector.js | 11 +- .../AlbumRelease/SelectAlbumReleaseModal.js | 37 - .../SelectAlbumReleaseModalContent.css | 18 - .../SelectAlbumReleaseModalContent.js | 93 - ...SelectAlbumReleaseModalContentConnector.js | 67 - .../AlbumRelease/SelectAlbumReleaseRow.css | 3 - .../AlbumRelease/SelectAlbumReleaseRow.js | 96 - .../SelectArtistModalContentConnector.js | 4 +- .../Confirmation/ConfirmImportModalContent.js | 2 +- .../ConfirmImportModalContentConnector.js | 2 +- .../InteractiveImportModalContent.js | 49 +- .../InteractiveImportModalContentConnector.js | 13 +- .../Interactive/InteractiveImportRow.js | 67 +- .../Track/SelectTrackModal.js | 37 - .../Track/SelectTrackModalContent.js | 236 -- .../Track/SelectTrackModalContentConnector.js | 112 - .../InteractiveImport/Track/SelectTrackRow.js | 121 - .../InteractiveSearch/InteractiveSearch.js | 27 +- .../InteractiveSearchFilterMenu.js | 39 + .../InteractiveSearchFilterMenuConnector.js | 49 + .../InteractiveSearchTable.js | 23 + .../OrganizePreviewModalContentConnector.js | 14 +- .../RetagPreviewModalContentConnector.js | 14 +- frontend/src/Search/AddNewItem.js | 12 +- .../Search/Album/AddNewAlbumModalContent.js | 2 +- .../Album/AddNewAlbumModalContentConnector.js | 9 +- .../Search/Album/AddNewAlbumSearchResult.js | 48 +- .../Search/Artist/AddNewArtistModalContent.js | 2 +- .../AddNewArtistModalContentConnector.js | 9 +- .../Search/Artist/AddNewArtistSearchResult.js | 15 +- .../src/Search/Common/AddArtistOptionsForm.js | 13 - ...tRemotePathMappingModalContentConnector.js | 16 +- .../ImportLists/EditImportListModalContent.js | 20 +- .../MediaManagement/MediaManagement.js | 2 + .../Settings/MediaManagement/Naming/Naming.js | 69 - .../RootFolder/EditRootFolderModalContent.js | 137 +- .../EditMetadataProfileModalContent.js | 121 +- ...ditMetadataProfileModalContentConnector.js | 121 +- .../Profiles/Metadata/MetadataProfile.js | 45 - .../Profiles/Metadata/PrimaryTypeItem.js | 60 - .../Profiles/Metadata/PrimaryTypeItems.js | 87 - .../Profiles/Metadata/ReleaseStatusItem.js | 60 - .../Profiles/Metadata/ReleaseStatusItems.js | 87 - .../Profiles/Metadata/SecondaryTypeItem.js | 60 - .../Profiles/Metadata/SecondaryTypeItems.js | 87 - .../Settings/Profiles/Metadata/TypeItem.css | 25 - .../Settings/Profiles/Metadata/TypeItems.css | 6 - .../Quality/Definition/QualityDefinition.js | 22 +- .../TagDetailsModalContentConnector.js | 2 +- frontend/src/Settings/Tags/Tag.js | 14 +- .../createBatchToggleAlbumMonitoredHandler.js | 10 +- .../src/Store/Actions/Settings/rootFolders.js | 5 + frontend/src/Store/Actions/albumActions.js | 43 +- .../src/Store/Actions/albumHistoryActions.js | 6 +- .../src/Store/Actions/albumStudioActions.js | 4 +- frontend/src/Store/Actions/artistActions.js | 6 +- .../src/Store/Actions/artistEditorActions.js | 4 +- .../src/Store/Actions/artistHistoryActions.js | 6 +- .../src/Store/Actions/blacklistActions.js | 2 +- frontend/src/Store/Actions/calendarActions.js | 4 +- frontend/src/Store/Actions/historyActions.js | 13 +- frontend/src/Store/Actions/index.js | 2 + .../Store/Actions/interactiveImportActions.js | 4 +- frontend/src/Store/Actions/queueActions.js | 12 +- frontend/src/Store/Actions/releaseActions.js | 2 +- frontend/src/Store/Actions/searchActions.js | 9 +- frontend/src/Store/Actions/seriesActions.js | 130 + frontend/src/Store/Actions/wantedActions.js | 38 +- .../Middleware/createSentryMiddleware.js | 4 +- .../Store/Selectors/createAlbumSelector.js | 6 +- .../Store/Selectors/createArtistSelector.js | 6 +- .../Selectors/createExistingArtistSelector.js | 6 +- .../createImportArtistItemSelector.js | 2 +- .../Selectors/createQueueItemSelector.js | 8 +- frontend/src/Styles/Variables/colors.js | 38 +- .../src/System/Status/MoreInfo/MoreInfo.js | 6 +- .../TrackFile/Editor/TrackFileEditorModal.js | 34 - .../TrackFile/Editor/TrackFileEditorRow.js | 8 - .../TrackFile/Editor/TrackFileEditorTable.js | 16 + ...nt.css => TrackFileEditorTableContent.css} | 0 ...tent.js => TrackFileEditorTableContent.js} | 172 +- ...> TrackFileEditorTableContentConnector.js} | 55 +- frontend/src/TrackFile/FileDetails.js | 2 +- .../UnmappedFilesTableConnector.js | 3 +- frontend/src/Utilities/Album/updateAlbums.js | 4 +- .../src/Utilities/Artist/monitorOptions.js | 12 +- .../src/Wanted/CutoffUnmet/CutoffUnmet.js | 4 +- .../CutoffUnmet/CutoffUnmetConnector.js | 6 +- .../src/Wanted/CutoffUnmet/CutoffUnmetRow.js | 28 +- frontend/src/Wanted/Missing/Missing.js | 4 +- .../src/Wanted/Missing/MissingConnector.js | 6 +- frontend/src/Wanted/Missing/MissingRow.js | 26 +- macOS/Readarr.app/Contents/Info.plist | 2 +- package.json | 95 +- setup/readarr.iss | 6 +- src/Directory.Build.props | 4 +- .../Http/HttpClientFixture.cs | 6 +- .../CleanseLogMessageFixture.cs | 2 +- .../Cloud/ReadarrCloudRequestBuilder.cs | 4 +- .../Extensions/FuzzyContains.cs | 10 +- src/NzbDrone.Common/Http/HttpClient.cs | 6 +- .../Instrumentation/NzbDroneLogger.cs | 6 +- src/NzbDrone.Console/Readarr.Console.csproj | 2 +- .../ArtistStatisticsFixture.cs | 60 +- .../BlacklistRepositoryFixture.cs | 8 +- .../Blacklisting/BlacklistServiceFixture.cs | 8 +- .../Datastore/DatabaseFixture.cs | 2 +- .../Datastore/DatabaseRelationshipFixture.cs | 33 +- .../Datastore/LazyLoadingFixture.cs | 110 +- .../Datastore/TableMapperFixture.cs | 2 +- .../Datastore/WhereBuilderFixture.cs | 47 +- .../AcceptableSizeSpecificationFixture.cs | 212 -- .../AlreadyImportedSpecificationFixture.cs | 16 +- .../CutoffSpecificationFixture.cs | 10 +- .../DiscographySpecificationFixture.cs | 8 +- .../DownloadDecisionMakerFixture.cs | 14 +- .../EarlyReleaseSpecificationFixture.cs | 14 +- .../HistorySpecificationFixture.cs | 24 +- .../MonitoredAlbumSpecificationFixture.cs | 16 +- .../PrioritizeDownloadDecisionFixture.cs | 102 +- .../ProtocolSpecificationFixture.cs | 2 +- ...ityAllowedByProfileSpecificationFixture.cs | 13 +- .../QueueSpecificationFixture.cs | 64 +- ...ReleaseRestrictionsSpecificationFixture.cs | 2 +- .../RepackSpecificationFixture.cs | 14 +- .../RssSync/DelaySpecificationFixture.cs | 40 +- .../DeletedTrackFileSpecificationFixture.cs | 41 +- .../RssSync/ProperSpecificationFixture.cs | 38 +- .../Search/ArtistSpecificationFixture.cs | 8 +- .../TorrentSeedingSpecificationFixture.cs | 4 +- .../UpgradeAllowedSpecificationFixture.cs | 4 +- .../UpgradeDiskSpecificationFixture.cs | 44 +- .../UpgradeSpecificationFixture.cs | 16 +- .../DiskSpace/DiskSpaceServiceFixture.cs | 6 +- .../ImportFixture.cs | 87 +- .../ProcessFixture.cs | 8 +- .../DownloadApprovedFixture.cs | 72 +- .../Blackhole/TorrentBlackholeFixture.cs | 12 +- .../Blackhole/UsenetBlackholeFixture.cs | 4 +- .../DownloadClientFixtureBase.cs | 4 +- .../PneumaticProviderFixture.cs | 6 +- .../QBittorrentTests/QBittorrentFixture.cs | 2 +- .../SabnzbdTests/SabnzbdFixture.cs | 2 +- .../UTorrentTests/UTorrentFixture.cs | 2 +- .../Download/DownloadServiceFixture.cs | 6 +- .../ProcessFailedFixture.cs | 4 +- .../ProcessFixture.cs | 4 +- .../PendingReleaseServiceTests/AddFixture.cs | 26 +- .../RemoveGrabbedFixture.cs | 28 +- .../RemovePendingFixture.cs | 14 +- .../RemoveRejectedFixture.cs | 24 +- .../RedownloadFailedDownloadServiceFixture.cs | 34 +- .../TrackedDownloadAlreadyImportedFixture.cs | 10 +- .../TrackedDownloadServiceFixture.cs | 12 +- .../Roksbox/FindMetadataFileFixture.cs | 78 - .../Consumers/Wdtv/FindMetadataFileFixture.cs | 52 - .../Consumers/Xbmc/FindMetadataFileFixture.cs | 65 - .../Checks/DeleteBadMediaCovers.cs | 4 +- .../Checks/RemotePathMappingCheckFixture.cs | 2 +- .../Checks/RootFolderCheckFixture.cs | 4 +- .../HistoryTests/HistoryRepositoryFixture.cs | 6 +- .../HistoryTests/HistoryServiceFixture.cs | 12 +- .../CleanupDuplicateMetadataFilesFixture.cs | 14 +- .../CleanupOrphanedAlbumsFixture.cs | 14 +- .../CleanupOrphanedBlacklistFixture.cs | 8 +- .../CleanupOrphanedHistoryItemsFixture.cs | 24 +- .../CleanupOrphanedMetadataFilesFixture.cs | 52 +- .../CleanupOrphanedPendingReleasesFixture.cs | 4 +- .../CleanupOrphanedTrackFilesFixture.cs | 31 +- .../CleanupOrphanedTracksFixture.cs | 43 - .../UpdateCleanTitleForArtistFixture.cs | 8 +- .../ImportListServiceFixture.cs | 6 +- .../ImportListSyncServiceFixture.cs | 110 +- .../Spotify/SpotifyFollowedArtistsFixture.cs | 191 - .../Spotify/SpotifyMappingFixture.cs | 345 -- .../Spotify/SpotifyPlaylistFixture.cs | 277 -- .../Spotify/SpotifySavedAlbumsFixture.cs | 187 - .../ArtistSearchServiceFixture.cs | 14 +- .../SearchDefinitionFixture.cs | 4 +- .../HeadphonesCapabilitiesProviderFixture.cs | 98 - .../HeadphonesTests/HeadphonesFixture.cs | 73 - .../NewznabRequestGeneratorFixture.cs | 2 +- .../WafflesTests/WafflesFixture.cs | 56 - .../Instrumentation/DatabaseTargetFixture.cs | 2 +- .../MediaCoverServiceFixture.cs | 18 +- .../MediaFiles/AudioTagServiceFixture.cs | 70 +- .../DiskScanServiceTests/ScanFixture.cs | 120 +- .../DownloadedAlbumsCommandServiceFixture.cs | 4 +- .../DownloadedTracksImportServiceFixture.cs | 6 +- .../MediaFiles/ImportApprovedTracksFixture.cs | 66 +- .../DeleteTrackFileFixture.cs | 8 +- .../MediaFiles/MediaFileRepositoryFixture.cs | 116 +- .../MediaFileServiceTests/FilterFixture.cs | 39 +- .../MediaFileServiceFixture.cs | 12 +- .../MediaFileTableCleanupServiceFixture.cs | 36 +- .../RenameTrackFileServiceFixture.cs | 16 +- .../MoveTrackFileFixture.cs | 16 +- .../Identification/AlbumDistanceFixture.cs | 263 -- .../Identification/CandidateServiceFixture.cs | 161 - .../IdentificationServiceFixture.cs | 33 +- .../Identification/TrackDistanceFixture.cs | 99 - .../Identification/TrackMappingFixture.cs | 188 - .../TrackImport/ImportDecisionMakerFixture.cs | 23 +- .../FreeSpaceSpecificationFixture.cs | 12 +- .../NotUnpackingSpecificationFixture.cs | 2 +- .../SameFileSpecificationFixture.cs | 51 +- .../UpgradeSpecificationFixture.cs | 22 +- .../UpgradeMediaFileServiceFixture.cs | 115 +- .../CommandEqualityComparerFixture.cs | 18 +- .../MetadataRequestBuilderFixture.cs | 4 +- .../SearchArtistComparerFixture.cs | 6 +- .../SkyHook/SkyHookProxyFixture.cs | 186 +- .../SkyHook/SkyHookProxySearchFixture.cs | 70 +- .../MusicTests/AddAlbumFixture.cs | 42 +- .../MusicTests/AddArtistFixture.cs | 72 +- .../AlbumMonitoredServiceFixture.cs | 38 +- .../AlbumRepositoryFixture.cs | 103 +- .../MusicTests/AlbumServiceFixture.cs | 10 +- .../ArtistMetadataRepositoryFixture.cs | 6 +- .../ArtistRepositoryFixture.cs | 38 +- .../FindByNameInexactFixture.cs | 12 +- .../UpdateMultipleArtistFixture.cs | 18 +- .../MusicTests/EntityFixture.cs | 135 +- .../MusicTests/MoveArtistServiceFixture.cs | 16 +- .../RefreshAlbumReleaseServiceFixture.cs | 138 - .../MusicTests/RefreshAlbumServiceFixture.cs | 342 +- .../MusicTests/RefreshArtistServiceFixture.cs | 111 +- .../MusicTests/RefreshTrackServiceFixture.cs | 54 - .../MusicTests/ShouldRefreshAlbumFixture.cs | 4 +- .../MusicTests/ShouldRefreshArtistFixture.cs | 8 +- .../NotificationBaseFixture.cs | 2 +- .../SynologyIndexerFixture.cs | 14 +- .../Xbmc/GetArtistPathFixture.cs | 91 - .../Xbmc/OnReleaseImportFixture.cs | 71 - .../NotificationTests/Xbmc/UpdateFixture.cs | 70 - .../OrganizerTests/BuildFilePathFixture.cs | 21 +- .../FileNameBuilderTests/CleanTitleFixture.cs | 46 +- .../FileNameBuilderFixture.cs | 275 +- .../FileNameBuilderTests/TitleTheFixture.cs | 29 +- .../OrganizerTests/GetAlbumFolderFixture.cs | 35 - .../OrganizerTests/GetArtistFolderFixture.cs | 2 +- .../ParserTests/ExtendedQualityParserRegex.cs | 7 +- .../ParserTests/HashedReleaseFixture.cs | 20 +- .../ParserTests/ParserFixture.cs | 10 +- .../ParsingServiceTests/GetAlbumsFixture.cs | 8 +- .../ParserTests/QualityParserFixture.cs | 286 +- .../MetadataProfileRepositoryFixture.cs | 31 +- .../Metadata/MetadataProfileServiceFixture.cs | 16 +- .../Profiles/ProfileRepositoryFixture.cs | 2 +- .../Profiles/ProfileServiceFixture.cs | 10 +- .../PreferredWordService/CalculateFixture.cs | 4 +- .../GetMatchingPreferredWordsFixture.cs | 4 +- .../GetAudioFilesFixture.cs | 8 +- .../QualityDefinitionServiceFixture.cs | 4 +- .../Qualities/QualityFixture.cs | 30 +- .../Qualities/QualityModelComparerFixture.cs | 32 +- .../QueueTests/QueueServiceFixture.cs | 8 +- .../UpdatePackageProviderFixture.cs | 3 +- .../UpdateTests/UpdateServiceFixture.cs | 7 +- .../SystemFolderValidatorFixture.cs | 12 +- .../ArtistStats/AlbumStatistics.cs | 4 +- .../ArtistStats/ArtistStatistics.cs | 2 +- .../ArtistStats/ArtistStatisticsRepository.cs | 37 +- .../ArtistStats/ArtistStatisticsService.cs | 16 +- src/NzbDrone.Core/Blacklisting/Blacklist.cs | 6 +- .../Blacklisting/BlacklistRepository.cs | 22 +- .../Blacklisting/BlacklistService.cs | 12 +- .../Books/Calibre/CalibreBook.cs | 34 + .../Books/Calibre/CalibreConversionOptions.cs | 87 + .../Books/Calibre/CalibreConversionStatus.cs | 18 + .../Books/Calibre/CalibreException.cs | 18 + .../Books/Calibre/CalibreImportJob.cs | 17 + .../Books/Calibre/CalibreProxy.cs | 320 ++ .../Books/Calibre/CalibreSettings.cs | 64 + .../Commands/BulkMoveArtistCommand.cs | 8 +- .../Commands/BulkRefreshArtistCommand.cs | 6 +- .../Commands/MoveArtistCommand.cs | 2 +- .../Commands/RefreshAlbumCommand.cs | 8 +- .../Commands/RefreshArtistCommand.cs | 8 +- .../Books/Events/AlbumAddedEvent.cs | 16 + .../Events/AlbumDeletedEvent.cs | 4 +- .../Events/AlbumEditedEvent.cs | 6 +- .../Books/Events/AlbumInfoRefreshedEvent.cs | 20 + .../Events/AlbumUpdatedEvent.cs | 4 +- .../Events/ArtistAddedEvent.cs | 4 +- .../Events/ArtistDeletedEvent.cs | 4 +- .../Events/ArtistEditedEvent.cs | 6 +- .../Events/ArtistMovedEvent.cs | 4 +- .../Events/ArtistRefreshCompleteEvent.cs | 4 +- .../Events/ArtistUpdatedEvent.cs | 4 +- .../Books/Events/ArtistsImportedEvent.cs | 17 + .../Handlers/AlbumAddedHandler.cs | 5 +- .../Handlers/ArtistAddedHandler.cs | 6 +- .../Handlers/ArtistEditedHandler.cs} | 0 .../Handlers/ArtistScannedHandler.cs | 2 +- .../{Music => Books}/Model/AddAlbumOptions.cs | 0 .../Model/AddArtistOptions.cs | 0 .../Model/ArtistStatusType.cs | 0 .../Model/Artist.cs => Books/Model/Author.cs} | 33 +- .../Model/AuthorMetadata.cs} | 25 +- .../Model/Album.cs => Books/Model/Book.cs} | 68 +- .../{Music => Books}/Model/Entity.cs | 0 .../{Music => Books}/Model/Links.cs | 0 .../Model/MonitoringOptions.cs | 0 .../{Music => Books}/Model/Ratings.cs | 0 src/NzbDrone.Core/Books/Model/Series.cs | 43 + .../Books/Model/SeriesBookLink.cs | 31 + .../Books/Repositories/AlbumRepository.cs | 200 ++ .../Repositories/ArtistMetadataRepository.cs | 25 +- .../Books/Repositories/ArtistRepository.cs | 68 + .../Repositories/SeriesBookLinkRepository.cs | 24 + .../Books/Repositories/SeriesRepository.cs | 48 + .../Services/AddAlbumService.cs | 53 +- .../Services/AddAuthorService.cs} | 34 +- .../Services/AlbumAddedService.cs | 10 +- .../Services/AlbumCutoffService.cs | 4 +- .../Services/AlbumMonitoredService.cs | 10 +- .../{Music => Books}/Services/AlbumService.cs | 150 +- .../Services/ArtistMetadataService.cs | 10 +- .../Services/ArtistService.cs | 112 +- .../Services/MoveArtistService.cs | 10 +- .../Services/RefreshAuthorService.cs} | 168 +- .../Books/Services/RefreshBookService.cs | 271 ++ .../Services/RefreshEntityServiceBase.cs | 51 +- .../Services/RefreshSeriesBookLinkService.cs | 44 + .../Books/Services/RefreshSeriesService.cs | 164 + .../Books/Services/SeriesBookLinkService.cs | 42 + .../Books/Services/SeriesService.cs | 60 + .../Utilities/AddArtistValidator.cs | 4 +- .../Utilities/ArtistPathBuilder.cs | 6 +- .../Utilities/ShouldRefreshAlbum.cs | 4 +- .../Utilities/ShouldRefreshArtist.cs | 4 +- src/NzbDrone.Core/Books/output.txt | 0 .../Datastore/BasicRepository.cs | 8 +- .../PrimaryAlbumTypeIntConverter.cs | 21 - .../Converters/ReleaseStatusIntConverter.cs | 21 - .../SecondaryAlbumTypeIntConverter.cs | 21 - .../Datastore/Extensions/BuilderExtensions.cs | 5 + .../Extensions/SqlMapperExtensions.cs | 8 + .../Datastore/Migration/001_initial_setup.cs | 164 +- src/NzbDrone.Core/Datastore/TableMapping.cs | 90 +- src/NzbDrone.Core/Datastore/WhereBuilder.cs | 15 +- .../DecisionEngine/DownloadDecisionMaker.cs | 30 +- .../DownloadDecisionPriorizationService.cs | 2 +- .../AcceptableSizeSpecification.cs | 21 +- .../Specifications/CutoffSpecification.cs | 53 +- .../RssSync/DelaySpecification.cs | 4 +- .../RssSync/DeletedTrackFileSpecification.cs | 2 +- .../SameTracksGrabSpecification.cs | 29 - .../Specifications/SameTracksSpecification.cs | 35 - .../UpgradeAllowedSpecification.cs | 46 +- .../UpgradeDiskSpecification.cs | 46 +- .../Clients/NzbVortex/NzbVortexSettings.cs | 2 +- .../Download/Clients/Nzbget/NzbgetSettings.cs | 2 +- .../Clients/Sabnzbd/SabnzbdSettings.cs | 2 +- .../Download/CompletedDownloadService.cs | 77 +- .../Download/DownloadFailedEvent.cs | 4 +- .../Download/DownloadIgnoredEvent.cs | 4 +- .../Download/FailedDownloadService.cs | 4 +- .../Download/IgnoredDownloadService.cs | 4 +- .../Download/Pending/PendingRelease.cs | 2 +- .../Pending/PendingReleaseRepository.cs | 12 +- .../Download/Pending/PendingReleaseService.cs | 46 +- .../Download/ProcessDownloadDecisions.cs | 4 +- .../RedownloadFailedDownloadService.cs | 12 +- .../TrackedDownloadAlreadyImported.cs | 2 +- .../TrackedDownloadService.cs | 14 +- .../Exceptions/AlbumNotFoundException.cs | 2 +- .../Exceptions/ArtistNotFoundException.cs | 2 +- src/NzbDrone.Core/Extras/ExtraService.cs | 26 +- src/NzbDrone.Core/Extras/Files/ExtraFile.cs | 4 +- .../Extras/Files/ExtraFileManager.cs | 28 +- .../Extras/Files/ExtraFileRepository.cs | 24 +- .../Extras/Files/ExtraFileService.cs | 6 +- .../Extras/IImportExistingExtraFiles.cs | 2 +- .../Extras/ImportExistingExtraFilesBase.cs | 8 +- .../Extras/Lyrics/ExistingLyricImporter.cs | 94 - .../Extras/Lyrics/ImportedLyricFiles.cs | 17 - src/NzbDrone.Core/Extras/Lyrics/LyricFile.cs | 8 - .../Extras/Lyrics/LyricFileExtensions.cs | 24 - .../Extras/Lyrics/LyricFileRepository.cs | 18 - .../Extras/Lyrics/LyricFileService.cs | 20 - .../Extras/Lyrics/LyricService.cs | 112 - .../Consumers/Roksbox/RoksboxMetadata.cs | 192 - .../Roksbox/RoksboxMetadataSettings.cs | 39 - .../Metadata/Consumers/Wdtv/WdtvMetadata.cs | 151 - .../Consumers/Wdtv/WdtvMetadataSettings.cs | 31 - .../Metadata/Consumers/Xbmc/XbmcMetadata.cs | 252 -- .../Consumers/Xbmc/XbmcMetadataSettings.cs | 43 - .../Consumers/Xbmc/XbmcNfoDetector.cs | 36 - .../Metadata/ExistingMetadataImporter.cs | 23 +- .../Files/CleanMetadataFileService.cs | 4 +- .../Extras/Metadata/IMetadata.cs | 18 +- .../Extras/Metadata/MetadataBase.cs | 18 +- .../Extras/Metadata/MetadataService.cs | 48 +- .../Others/ExistingOtherExtraImporter.cs | 17 +- .../Extras/Others/OtherExtraFileRenamer.cs | 6 +- .../Extras/Others/OtherExtraService.cs | 10 +- .../HealthCheck/Checks/MonoVersionCheck.cs | 37 +- src/NzbDrone.Core/History/History.cs | 10 +- .../History/HistoryRepository.cs | 50 +- src/NzbDrone.Core/History/HistoryService.cs | 207 +- .../CleanupDuplicateMetadataFiles.cs | 8 +- .../Housekeepers/CleanupOrphanedAlbums.cs | 14 +- .../CleanupOrphanedArtistMetadata.cs | 15 +- .../Housekeepers/CleanupOrphanedBlacklist.cs | 6 +- .../CleanupOrphanedHistoryItems.cs | 12 +- .../CleanupOrphanedMetadataFiles.cs | 26 +- .../CleanupOrphanedPendingReleases.cs | 6 +- .../Housekeepers/CleanupOrphanedReleases.cs | 28 - .../Housekeepers/CleanupOrphanedTrackFiles.cs | 25 +- .../Housekeepers/CleanupOrphanedTracks.cs | 28 - .../Housekeepers/CleanupUnusedTags.cs | 2 +- .../Exclusions/ImportListExclusionService.cs | 10 +- .../Goodreads/GoodreadsBookshelf.cs | 149 + .../Goodreads/GoodreadsBookshelfSettings.cs | 28 + .../Goodreads/GoodreadsException.cs | 31 + .../Goodreads/GoodreadsImportListBase.cs | 172 + .../Goodreads/GoodreadsOwnedBooks.cs | 82 + .../Goodreads/GoodreadsSettingsBase.cs | 58 + .../HeadphonesImport/HeadphonesImportApi.cs | 8 - .../HeadphonesImportRequestGenerator.cs | 33 - .../HeadphonesImportSettings.cs | 35 - .../ImportListSyncCompleteEvent.cs | 4 +- .../ImportLists/ImportListSyncService.cs | 88 +- .../ImportLists/ImportListType.cs | 3 +- .../ImportLists/LastFm/LastFmApi.cs | 20 - .../ImportLists/LastFm/LastFmParser.cs | 60 - .../ImportLists/LastFm/LastFmTag.cs | 31 - .../ImportLists/LastFm/LastFmTagSettings.cs | 41 - .../ImportLists/LastFm/LastFmUser.cs | 31 - .../LastFm/LastFmUserRequestGenerator.cs | 33 - .../ImportLists/LastFm/LastFmUserSettings.cs | 41 - .../LazyLibrarianImport.cs} | 12 +- .../LazyLibrarian/LazyLibrarianImportApi.cs | 11 + .../LazyLibrarianImportParser.cs} | 11 +- .../LazyLibrarianImportRequestGenerator.cs} | 10 +- .../LazyLibrarianImportSettings.cs | 36 + .../ImportLists/ReadarrLists/ReadarrLists.cs | 69 - .../ReadarrLists/ReadarrListsApi.cs | 13 - .../ReadarrLists/ReadarrListsParser.cs | 70 - .../ReadarrListsRequestGenerator.cs | 36 - .../ReadarrLists/ReadarrListsSettings.cs | 33 - .../ImportLists/Spotify/SpotifyException.cs | 31 - .../Spotify/SpotifyFollowedArtists.cs | 79 - .../Spotify/SpotifyImportListBase.cs | 354 -- .../Spotify/SpotifyImportListItemInfo.cs | 15 - .../ImportLists/Spotify/SpotifyMap.cs | 8 - .../ImportLists/Spotify/SpotifyPlaylist.cs | 160 - .../Spotify/SpotifyPlaylistSettings.cs | 30 - .../ImportLists/Spotify/SpotifyProxy.cs | 109 - .../ImportLists/Spotify/SpotifySavedAlbums.cs | 86 - .../Spotify/SpotifySettingsBase.cs | 55 - .../IndexerSearch/AlbumSearchCommand.cs | 6 +- .../IndexerSearch/AlbumSearchService.cs | 28 +- .../IndexerSearch/ArtistSearchCommand.cs | 2 +- .../IndexerSearch/ArtistSearchService.cs | 2 +- .../CutoffUnmetAlbumSearchCommand.cs | 6 +- .../Definitions/SearchCriteriaBase.cs | 8 +- .../MissingAlbumSearchCommand.cs | 6 +- .../IndexerSearch/NzbSearchService.cs | 24 +- src/NzbDrone.Core/Indexers/Gazelle/Gazelle.cs | 11 - .../Indexers/Headphones/Headphones.cs | 79 - .../Headphones/HeadphonesCapabilities.cs | 21 - .../HeadphonesCapabilitiesProvider.cs | 154 - .../Headphones/HeadphonesRequestGenerator.cs | 102 - .../Headphones/HeadphonesRssParser.cs | 22 - .../Indexers/Headphones/HeadphonesSettings.cs | 61 - .../Indexers/Newznab/NewznabSettings.cs | 2 +- src/NzbDrone.Core/Indexers/Waffles/Waffles.cs | 30 - .../Waffles/WafflesRequestGenerator.cs | 63 - .../Indexers/Waffles/WafflesRssParser.cs | 93 - .../Indexers/Waffles/WafflesSettings.cs | 50 - .../CoverAlreadyExistsSpecification.cs | 2 +- .../MediaCover/MediaCoverService.cs | 41 +- .../MediaCover/MediaCoversUpdatedEvent.cs | 8 +- src/NzbDrone.Core/MediaFiles/AudioTag.cs | 76 +- .../MediaFiles/AudioTagService.cs | 211 +- .../MediaFiles/AzwTag/Azw3File.cs | 26 + .../MediaFiles/AzwTag/Headers.cs | 135 + .../MediaFiles/AzwTag/ProcessSection.cs | 47 + .../MediaFiles/AzwTag/Structs.cs | 140 + src/NzbDrone.Core/MediaFiles/AzwTag/Utils.cs | 88 + .../MediaFiles/{TrackFile.cs => BookFile.cs} | 10 +- .../Commands/RenameArtistCommand.cs | 4 +- .../MediaFiles/Commands/RenameFilesCommand.cs | 6 +- .../Commands/RescanFoldersCommand.cs | 6 +- .../MediaFiles/Commands/RetagArtistCommand.cs | 4 +- .../MediaFiles/Commands/RetagFilesCommand.cs | 6 +- .../MediaFiles/DiskScanService.cs | 66 +- .../DownloadedTracksImportService.cs | 12 +- .../MediaFiles/EbookTagService.cs | 252 ++ .../MediaFiles/EpubTag/Entities/EpubBook.cs | 13 + .../MediaFiles/EpubTag/Entities/EpubSchema.cs | 10 + .../MediaFiles/EpubTag/EpubReader.cs | 69 + .../EpubTag/Readers/PackageReader.cs | 280 ++ .../EpubTag/Readers/RootFilePathReader.cs | 35 + .../EpubTag/Readers/SchemaReader.cs | 20 + .../EpubTag/RefEntities/EpubBookRef.cs | 49 + .../EpubTag/Schema/Common/ManifestProperty.cs | 37 + .../Common/StructuralSemanticsProperty.cs | 409 +++ .../EpubTag/Schema/Opf/EpubMetadata.cs | 24 + .../Schema/Opf/EpubMetadataContributor.cs | 9 + .../EpubTag/Schema/Opf/EpubMetadataCreator.cs | 9 + .../EpubTag/Schema/Opf/EpubMetadataDate.cs | 8 + .../Schema/Opf/EpubMetadataIdentifier.cs | 9 + .../EpubTag/Schema/Opf/EpubMetadataMeta.cs | 12 + .../EpubTag/Schema/Opf/EpubPackage.cs | 15 + .../EpubTag/Schema/Opf/EpubVersion.cs | 26 + .../Schema/Opf/PageProgressionDirection.cs | 28 + .../EpubTag/Utils/StringExtensionMethods.cs | 12 + .../MediaFiles/EpubTag/Utils/VersionUtils.cs | 23 + .../EpubTag/Utils/XmlExtensionMethods.cs | 27 + .../MediaFiles/EpubTag/Utils/XmlUtils.cs | 58 + .../MediaFiles/EpubTag/Utils/ZipPathUtils.cs | 37 + .../MediaFiles/Events/AlbumImportedEvent.cs | 12 +- .../MediaFiles/Events/ArtistRenamedEvent.cs | 4 +- .../Events/ArtistScanSkippedEvent.cs | 4 +- .../MediaFiles/Events/ArtistScannedEvent.cs | 4 +- .../MediaFiles/Events/TrackFileAddedEvent.cs | 4 +- .../Events/TrackFileDeletedEvent.cs | 4 +- .../Events/TrackFileRenamedEvent.cs | 6 +- .../Events/TrackFileRetaggedEvent.cs | 8 +- .../Events/TrackFolderCreatedEvent.cs | 6 +- .../MediaFiles/Events/TrackImportedEvent.cs | 6 +- .../MediaFiles/MediaFileDeletionService.cs | 8 +- .../MediaFiles/MediaFileExtensions.cs | 40 +- .../MediaFiles/MediaFileRepository.cs | 130 +- .../MediaFiles/MediaFileService.cs | 76 +- .../MediaFileTableCleanupService.cs | 9 - .../MediaFiles/RenameTrackFilePreview.cs | 4 +- .../MediaFiles/RenameTrackFileService.cs | 63 +- .../MediaFiles/RetagTrackFilePreview.cs | 4 +- .../MediaFiles/RootFolderWatchingService.cs | 2 +- .../MediaFiles/TrackFileMoveResult.cs | 6 +- .../MediaFiles/TrackFileMovingService.cs | 49 +- .../Aggregation/AggregationService.cs | 2 +- .../Aggregators/AggregateCalibreData.cs | 45 + .../Identification/CandidateAlbumRelease.cs | 10 +- .../Identification/CandidateService.cs | 192 +- .../TrackImport/Identification/Distance.cs | 23 +- .../Identification/DistanceCalcualtor.cs | 205 -- .../Identification/DistanceCalculator.cs | 87 + .../Identification/IdentificationService.cs | 201 +- .../Identification/TrackGroupingService.cs | 9 +- .../TrackImport/ImportApprovedTracks.cs | 125 +- .../TrackImport/ImportDecisionMaker.cs | 27 +- .../TrackImport/Manual/ManualImportFile.cs | 8 +- .../TrackImport/Manual/ManualImportItem.cs | 8 +- .../TrackImport/Manual/ManualImportService.cs | 58 +- .../AlbumUpgradeSpecification.cs | 41 +- .../AlreadyImportedSpecification.cs | 12 +- .../ArtistPathInRootFolderSpecification.cs | 2 +- .../CloseAlbumMatchSpecification.cs | 19 - .../CloseTrackMatchSpecification.cs | 33 - .../Specifications/MoreTracksSpecification.cs | 33 - ...NoMissingOrUnmatchedTracksSpecification.cs | 34 - .../ReleaseWantedSpecification.cs | 28 - .../Specifications/SameFileSpecification.cs | 19 +- .../SameTracksImportSpecification.cs | 33 - .../Specifications/UpgradeSpecification.cs | 13 +- .../MediaFiles/UpdateTrackFileService.cs | 43 +- .../MediaFiles/UpgradeMediaFileService.cs | 135 +- .../MetadataSource/IProvideAlbumInfo.cs | 4 +- ...ideArtistInfo.cs => IProvideAuthorInfo.cs} | 4 +- .../MetadataSource/ISearchForNewAlbum.cs | 9 +- .../MetadataSource/ISearchForNewArtist.cs | 4 +- .../MetadataSource/SearchArtistComparer.cs | 6 +- .../Extensions/HttpResponseExtensions.cs | 117 + .../SkyHook/Extensions/XmlExtensions.cs | 241 ++ .../AuthorBookListResource.cs | 27 + .../GoodreadsResource/AuthorResource.cs | 145 + .../AuthorSeriesListResource.cs | 51 + .../AuthorSummaryResource.cs | 73 + .../GoodreadsResource/BestBookResource.cs | 57 + .../GoodreadsResource/BookLinkResource.cs | 56 + .../SkyHook/GoodreadsResource/BookResource.cs | 239 ++ .../BookSearchResultResource.cs | 27 + .../GoodreadsResource/BookSummaryResource.cs | 150 + .../GoodreadsResource/GoodreadsResource.cs | 11 + .../GoodreadsResource/OwnedBookResource.cs | 82 + .../GoodreadsResource/PaginatedList.cs | 47 + .../GoodreadsResource/PaginationModel.cs | 58 + .../GoodreadsResource/ReviewResource.cs | 131 + .../GoodreadsResource/SeriesResource.cs | 72 + .../GoodreadsResource/UserShelfResource.cs | 113 + .../SkyHook/GoodreadsResource/WorkResource.cs | 168 + .../SkyHook/Resource/AlbumResource.cs | 25 - .../SkyHook/Resource/ArtistResource.cs | 28 - .../SkyHook/Resource/EntityResource.cs | 9 - .../SkyHook/Resource/ImageResource.cs | 10 - .../SkyHook/Resource/LinkResource.cs | 8 - .../SkyHook/Resource/MediumResource.cs | 9 - .../SkyHook/Resource/RatingResource.cs | 8 - .../SkyHook/Resource/RecentUpdatesResource.cs | 13 - .../SkyHook/Resource/ReleaseResource.cs | 20 - .../SkyHook/Resource/TrackResource.cs | 23 - .../MetadataSource/SkyHook/SkyHookProxy.cs | 644 ++-- .../SkyHook/SkyHookResource/AuthorResource.cs | 7 + .../SkyHookResource/AuthorSummaryResource.cs | 19 + .../SkyHook/SkyHookResource/BookResource.cs | 40 + .../SkyHookResource/BookSearchResource.cs | 6 + .../SkyHookResource/BulkResourceBase.cs | 11 + .../SkyHookResource/ContributorResource.cs | 8 + .../SkyHook/SkyHookResource/SeriesResource.cs | 19 + .../Music/Events/AlbumAddedEvent.cs | 14 - .../Music/Events/AlbumInfoRefreshedEvent.cs | 20 - .../Music/Events/ArtistsImportedEvent.cs | 15 - .../Music/Events/ReleaseDeletedEvent.cs | 14 - src/NzbDrone.Core/Music/Model/Medium.cs | 12 - src/NzbDrone.Core/Music/Model/Member.cs | 18 - .../Music/Model/PrimaryAlbumType.cs | 122 - src/NzbDrone.Core/Music/Model/Release.cs | 68 - .../Music/Model/ReleaseStatus.cs | 120 - .../Music/Model/SecondaryAlbumType.cs | 133 - src/NzbDrone.Core/Music/Model/Track.cs | 86 - .../Music/Repositories/AlbumRepository.cs | 216 -- .../Music/Repositories/ArtistRepository.cs | 75 - .../Music/Repositories/ReleaseRepository.cs | 78 - .../Music/Repositories/TrackRepository.cs | 117 - .../Music/Services/AlbumEditedService.cs | 42 - .../Services/RefreshAlbumReleaseService.cs | 122 - .../Music/Services/RefreshAlbumService.cs | 365 -- .../Music/Services/RefreshTrackService.cs | 76 - .../Music/Services/ReleaseService.cs | 95 - .../Music/Services/TrackService.cs | 137 - .../Notifications/AlbumDownloadMessage.cs | 9 +- .../CustomScript/CustomScript.cs | 21 +- .../Notifications/Discord/Discord.cs | 2 +- .../Notifications/GrabMessage.cs | 2 +- .../Notifications/INotification.cs | 2 +- .../MediaBrowser/MediaBrowser.cs | 74 - .../MediaBrowser/MediaBrowserProxy.cs | 115 - .../MediaBrowser/MediaBrowserService.cs | 67 - .../MediaBrowser/MediaBrowserSettings.cs | 55 - .../MediaBrowser/Model/EmbyMediaFolder.cs | 8 - .../Model/EmbyMediaFoldersResponse.cs | 9 - .../MediaBrowser/Model/EmbyMediaUpdateInfo.cs | 8 - .../Notifications/NotificationBase.cs | 2 +- .../Notifications/NotificationService.cs | 10 +- .../Plex/PlexAuthenticationException.cs | 15 - .../Notifications/Plex/PlexException.cs | 23 - .../Plex/PlexTv/PlexTvPinResponse.cs | 9 - .../Plex/PlexTv/PlexTvPinUrlResponse.cs | 11 - .../Notifications/Plex/PlexTv/PlexTvProxy.cs | 79 - .../Plex/PlexTv/PlexTvService.cs | 83 - .../Plex/PlexTv/PlexTvSignInUrlResponse.cs | 8 - .../Plex/PlexVersionException.cs | 17 - .../Notifications/Plex/Server/PlexError.cs | 7 - .../Notifications/Plex/Server/PlexIdentity.cs | 8 - .../Plex/Server/PlexPreferences.cs | 24 - .../Notifications/Plex/Server/PlexResponse.cs | 7 - .../Notifications/Plex/Server/PlexSection.cs | 57 - .../Plex/Server/PlexSectionItem.cs | 25 - .../Notifications/Plex/Server/PlexServer.cs | 107 - .../Plex/Server/PlexServerProxy.cs | 224 -- .../Plex/Server/PlexServerService.cs | 183 - .../Plex/Server/PlexServerSettings.cs | 53 - .../Notifications/Slack/Slack.cs | 2 +- .../Notifications/Subsonic/Subsonic.cs | 2 +- .../Notifications/Synology/SynologyIndexer.cs | 2 +- .../Notifications/TrackRetagMessage.cs | 7 +- .../Notifications/Webhook/Webhook.cs | 10 +- .../Notifications/Webhook/WebhookAlbum.cs | 2 +- .../Notifications/Webhook/WebhookArtist.cs | 4 +- .../Webhook/WebhookImportPayload.cs | 2 +- .../Notifications/Webhook/WebhookTrack.cs | 26 - .../Notifications/Webhook/WebhookTrackFile.cs | 2 +- .../Notifications/Xbmc/Model/ActivePlayer.cs | 14 - .../Xbmc/Model/ActivePlayersResult.cs | 11 - .../Xbmc/Model/ArtistResponse.cs | 9 - .../Notifications/Xbmc/Model/ArtistResult.cs | 15 - .../Notifications/Xbmc/Model/ErrorResult.cs | 11 - .../Notifications/Xbmc/Model/KodiArtist.cs | 12 - .../Xbmc/Model/XbmcJsonResult.cs | 9 - src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs | 102 - .../Notifications/Xbmc/XbmcJsonApiProxy.cs | 143 - .../Notifications/Xbmc/XbmcJsonException.cs | 16 - .../Notifications/Xbmc/XbmcService.cs | 121 - .../Notifications/Xbmc/XbmcSettings.cs | 65 - .../Organizer/FileNameBuilder.cs | 163 +- .../Organizer/FileNameSampleService.cs | 96 +- .../Organizer/FileNameValidation.cs | 14 +- src/NzbDrone.Core/Organizer/NamingConfig.cs | 7 +- src/NzbDrone.Core/Organizer/SampleResult.cs | 8 +- .../Parser/Model/LocalAlbumRelease.cs | 29 +- src/NzbDrone.Core/Parser/Model/LocalTrack.cs | 11 +- .../Parser/Model/ParsedTrackInfo.cs | 9 +- src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs | 6 +- src/NzbDrone.Core/Parser/Parser.cs | 149 +- src/NzbDrone.Core/Parser/ParsingService.cs | 92 +- src/NzbDrone.Core/Parser/QualityParser.cs | 499 +-- .../Profiles/Metadata/MetadataProfile.cs | 11 +- .../Metadata/MetadataProfileService.cs | 112 +- .../Metadata/ProfilePrimaryAlbumTypeItem.cs | 11 - .../Metadata/ProfileReleaseStatusTypeItem.cs | 11 - .../Metadata/ProfileSecondaryAlbumTypeItem.cs | 11 - .../Qualities/QualityProfileService.cs | 69 +- .../Profiles/Releases/PreferredWordService.cs | 8 +- src/NzbDrone.Core/Qualities/Quality.cs | 124 +- src/NzbDrone.Core/Qualities/Revision.cs | 2 +- src/NzbDrone.Core/Queue/Queue.cs | 4 +- src/NzbDrone.Core/Queue/QueueService.cs | 2 +- src/NzbDrone.Core/Readarr.Core.csproj | 3 +- src/NzbDrone.Core/RootFolders/RootFolder.cs | 3 + .../RootFolders/RootFolderRepository.cs | 38 +- src/NzbDrone.Core/Tags/TagDetails.cs | 4 +- src/NzbDrone.Core/Tags/TagService.cs | 4 +- .../Validation/Paths/ArtistExistsValidator.cs | 2 +- src/NzbDrone.Host/NzbDrone.ico | Bin 370070 -> 0 bytes src/NzbDrone.Host/Readarr.ico | Bin 0 -> 324065 bytes .../ApiTests/ArtistEditorFixture.cs | 2 +- .../ApiTests/ArtistFixture.cs | 38 +- .../ApiTests/ArtistLookupFixture.cs | 19 +- .../ApiTests/BlacklistFixture.cs | 4 +- .../ApiTests/CalendarFixture.cs | 28 +- .../ApiTests/FileSystemFixture.cs | 4 +- .../ApiTests/NotificationFixture.cs | 6 +- .../ApiTests/ReleasePushFixture.cs | 2 +- .../ApiTests/TrackFixture.cs | 43 - .../ApiTests/WantedFixture.cs | 38 +- .../Client/AlbumClient.cs | 4 +- .../Client/TrackClient.cs | 20 - .../IntegrationTestBase.cs | 36 +- src/NzbDrone/Properties/Resources.resx | 2 +- src/NzbDrone/Readarr.csproj | 2 +- .../Albums/AlbumLookupModule.cs | 8 +- src/Readarr.Api.V1/Albums/AlbumModule.cs | 57 +- .../Albums/AlbumModuleWithSignalR.cs | 20 +- .../Albums/AlbumReleaseResource.cs | 107 - src/Readarr.Api.V1/Albums/AlbumResource.cs | 82 +- .../Albums/AlbumsMonitoredResource.cs | 2 +- src/Readarr.Api.V1/Albums/MediumResource.cs | 56 - .../RenameBookModule.cs} | 18 +- .../RenameBookResource.cs} | 12 +- .../RetagBookModule.cs} | 16 +- .../RetagBookResource.cs} | 10 +- .../Artist/ArtistEditorDeleteResource.cs | 2 +- .../Artist/ArtistEditorModule.cs | 13 +- .../Artist/ArtistEditorResource.cs | 2 +- .../Artist/ArtistLookupModule.cs | 8 +- src/Readarr.Api.V1/Artist/ArtistModule.cs | 16 +- src/Readarr.Api.V1/Artist/ArtistResource.cs | 39 +- .../Blacklist/BlacklistResource.cs | 8 +- .../Calendar/CalendarFeedModule.cs | 4 +- .../Config/NamingConfigModule.cs | 23 +- .../Config/NamingConfigResource.cs | 2 - .../Config/NamingExampleResource.cs | 9 +- src/Readarr.Api.V1/History/HistoryModule.cs | 41 +- src/Readarr.Api.V1/History/HistoryResource.cs | 12 +- src/Readarr.Api.V1/Indexers/ReleaseModule.cs | 16 +- .../Indexers/ReleaseResource.cs | 4 +- .../ManualImport/ManualImportModule.cs | 7 +- .../ManualImport/ManualImportResource.cs | 9 +- .../MediaCovers/MediaCoverModule.cs | 16 +- .../Metadata/MetadataProfileModule.cs | 3 - .../Metadata/MetadataProfileResource.cs | 132 +- .../Metadata/MetadataProfileSchemaModule.cs | 29 +- .../Profiles/Metadata/MetadataValidator.cs | 106 - .../Queue/QueueDetailsModule.cs | 16 +- src/Readarr.Api.V1/Queue/QueueModule.cs | 4 +- src/Readarr.Api.V1/Queue/QueueResource.cs | 8 +- .../RootFolders/RootFolderModule.cs | 12 + .../RootFolders/RootFolderResource.cs | 43 +- src/Readarr.Api.V1/Search/SearchModule.cs | 12 +- .../Series/SeriesBookLinkResource.cs | 33 + src/Readarr.Api.V1/Series/SeriesModule.cs | 35 + src/Readarr.Api.V1/Series/SeriesResource.cs | 37 + src/Readarr.Api.V1/Tags/TagDetailsResource.cs | 4 +- .../TrackFiles/TrackFileModule.cs | 36 +- .../TrackFiles/TrackFileResource.cs | 14 +- src/Readarr.Api.V1/Tracks/TrackModule.cs | 65 - .../Tracks/TrackModuleWithSignalR.cs | 124 - src/Readarr.Api.V1/Tracks/TrackResource.cs | 70 - src/Readarr.Api.V1/Wanted/CutoffModule.cs | 6 +- src/Readarr.Api.V1/Wanted/MissingModule.cs | 6 +- yarn.lock | 3149 ++++++++++------- 911 files changed, 14827 insertions(+), 24432 deletions(-) delete mode 100644 frontend/src/Album/Details/AlbumDetailsMedium.js delete mode 100644 frontend/src/Album/Details/AlbumDetailsMediumConnector.js delete mode 100644 frontend/src/Album/Details/TrackActionsCell.css delete mode 100644 frontend/src/Album/Details/TrackActionsCell.js delete mode 100644 frontend/src/Album/Details/TrackRow.css delete mode 100644 frontend/src/Album/Details/TrackRow.js delete mode 100644 frontend/src/Album/Details/TrackRowConnector.js delete mode 100644 frontend/src/Album/Edit/EditAlbumModal.js delete mode 100644 frontend/src/Album/Edit/EditAlbumModalConnector.js delete mode 100644 frontend/src/Album/Edit/EditAlbumModalContent.js delete mode 100644 frontend/src/Album/Edit/EditAlbumModalContentConnector.js delete mode 100644 frontend/src/Artist/ArtistLogo.js rename frontend/src/{Album/Details/AlbumDetailsMedium.css => Artist/Details/AuthorDetailsSeries.css} (74%) create mode 100644 frontend/src/Artist/Details/AuthorDetailsSeries.js create mode 100644 frontend/src/Artist/Details/AuthorDetailsSeriesConnector.js rename frontend/src/Artist/History/{ArtistHistoryModalContentConnector.js => ArtistHistoryContentConnector.js} (71%) create mode 100644 frontend/src/Artist/History/ArtistHistoryTable.js create mode 100644 frontend/src/Artist/History/ArtistHistoryTableContent.js delete mode 100644 frontend/src/Artist/Search/ArtistInteractiveSearchModal.js delete mode 100644 frontend/src/Artist/Search/ArtistInteractiveSearchModalConnector.js delete mode 100644 frontend/src/Artist/Search/ArtistInteractiveSearchModalContent.js delete mode 100644 frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModal.js delete mode 100644 frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContent.css delete mode 100644 frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContent.js delete mode 100644 frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContentConnector.js delete mode 100644 frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.css delete mode 100644 frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js delete mode 100644 frontend/src/InteractiveImport/Track/SelectTrackModal.js delete mode 100644 frontend/src/InteractiveImport/Track/SelectTrackModalContent.js delete mode 100644 frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js delete mode 100644 frontend/src/InteractiveImport/Track/SelectTrackRow.js create mode 100644 frontend/src/InteractiveSearch/InteractiveSearchFilterMenu.js create mode 100644 frontend/src/InteractiveSearch/InteractiveSearchFilterMenuConnector.js create mode 100644 frontend/src/InteractiveSearch/InteractiveSearchTable.js delete mode 100644 frontend/src/Settings/Profiles/Metadata/PrimaryTypeItem.js delete mode 100644 frontend/src/Settings/Profiles/Metadata/PrimaryTypeItems.js delete mode 100644 frontend/src/Settings/Profiles/Metadata/ReleaseStatusItem.js delete mode 100644 frontend/src/Settings/Profiles/Metadata/ReleaseStatusItems.js delete mode 100644 frontend/src/Settings/Profiles/Metadata/SecondaryTypeItem.js delete mode 100644 frontend/src/Settings/Profiles/Metadata/SecondaryTypeItems.js delete mode 100644 frontend/src/Settings/Profiles/Metadata/TypeItem.css delete mode 100644 frontend/src/Settings/Profiles/Metadata/TypeItems.css create mode 100644 frontend/src/Store/Actions/seriesActions.js delete mode 100644 frontend/src/TrackFile/Editor/TrackFileEditorModal.js create mode 100644 frontend/src/TrackFile/Editor/TrackFileEditorTable.js rename frontend/src/TrackFile/Editor/{TrackFileEditorModalContent.css => TrackFileEditorTableContent.css} (100%) rename frontend/src/TrackFile/Editor/{TrackFileEditorModalContent.js => TrackFileEditorTableContent.js} (59%) rename frontend/src/TrackFile/Editor/{TrackFileEditorModalContentConnector.js => TrackFileEditorTableContentConnector.js} (70%) delete mode 100644 src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs delete mode 100644 src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs delete mode 100644 src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs delete mode 100644 src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Xbmc/FindMetadataFileFixture.cs delete mode 100644 src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTracksFixture.cs delete mode 100644 src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyFollowedArtistsFixture.cs delete mode 100644 src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyMappingFixture.cs delete mode 100644 src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyPlaylistFixture.cs delete mode 100644 src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifySavedAlbumsFixture.cs delete mode 100644 src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesCapabilitiesProviderFixture.cs delete mode 100644 src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesFixture.cs delete mode 100644 src/NzbDrone.Core.Test/IndexerTests/WafflesTests/WafflesFixture.cs delete mode 100644 src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/AlbumDistanceFixture.cs delete mode 100644 src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/CandidateServiceFixture.cs delete mode 100644 src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/TrackDistanceFixture.cs delete mode 100644 src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/TrackMappingFixture.cs delete mode 100644 src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs delete mode 100644 src/NzbDrone.Core.Test/MusicTests/RefreshTrackServiceFixture.cs delete mode 100644 src/NzbDrone.Core.Test/NotificationTests/Xbmc/GetArtistPathFixture.cs delete mode 100644 src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnReleaseImportFixture.cs delete mode 100644 src/NzbDrone.Core.Test/NotificationTests/Xbmc/UpdateFixture.cs delete mode 100644 src/NzbDrone.Core.Test/OrganizerTests/GetAlbumFolderFixture.cs create mode 100644 src/NzbDrone.Core/Books/Calibre/CalibreBook.cs create mode 100644 src/NzbDrone.Core/Books/Calibre/CalibreConversionOptions.cs create mode 100644 src/NzbDrone.Core/Books/Calibre/CalibreConversionStatus.cs create mode 100644 src/NzbDrone.Core/Books/Calibre/CalibreException.cs create mode 100644 src/NzbDrone.Core/Books/Calibre/CalibreImportJob.cs create mode 100644 src/NzbDrone.Core/Books/Calibre/CalibreProxy.cs create mode 100644 src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs rename src/NzbDrone.Core/{Music => Books}/Commands/BulkMoveArtistCommand.cs (83%) rename src/NzbDrone.Core/{Music => Books}/Commands/BulkRefreshArtistCommand.cs (77%) rename src/NzbDrone.Core/{Music => Books}/Commands/MoveArtistCommand.cs (89%) rename src/NzbDrone.Core/{Music => Books}/Commands/RefreshAlbumCommand.cs (59%) rename src/NzbDrone.Core/{Music => Books}/Commands/RefreshArtistCommand.cs (65%) create mode 100644 src/NzbDrone.Core/Books/Events/AlbumAddedEvent.cs rename src/NzbDrone.Core/{Music => Books}/Events/AlbumDeletedEvent.cs (73%) rename src/NzbDrone.Core/{Music => Books}/Events/AlbumEditedEvent.cs (56%) create mode 100644 src/NzbDrone.Core/Books/Events/AlbumInfoRefreshedEvent.cs rename src/NzbDrone.Core/{Music => Books}/Events/AlbumUpdatedEvent.cs (65%) rename src/NzbDrone.Core/{Music => Books}/Events/ArtistAddedEvent.cs (70%) rename src/NzbDrone.Core/{Music => Books}/Events/ArtistDeletedEvent.cs (79%) rename src/NzbDrone.Core/{Music => Books}/Events/ArtistEditedEvent.cs (56%) rename src/NzbDrone.Core/{Music => Books}/Events/ArtistMovedEvent.cs (78%) rename src/NzbDrone.Core/{Music => Books}/Events/ArtistRefreshCompleteEvent.cs (65%) rename src/NzbDrone.Core/{Music => Books}/Events/ArtistUpdatedEvent.cs (64%) create mode 100644 src/NzbDrone.Core/Books/Events/ArtistsImportedEvent.cs rename src/NzbDrone.Core/{Music => Books}/Handlers/AlbumAddedHandler.cs (77%) rename src/NzbDrone.Core/{Music => Books}/Handlers/ArtistAddedHandler.cs (84%) rename src/NzbDrone.Core/{Music/Services/ArtistEditedService.cs => Books/Handlers/ArtistEditedHandler.cs} (100%) rename src/NzbDrone.Core/{Music => Books}/Handlers/ArtistScannedHandler.cs (97%) rename src/NzbDrone.Core/{Music => Books}/Model/AddAlbumOptions.cs (100%) rename src/NzbDrone.Core/{Music => Books}/Model/AddArtistOptions.cs (100%) rename src/NzbDrone.Core/{Music => Books}/Model/ArtistStatusType.cs (100%) rename src/NzbDrone.Core/{Music/Model/Artist.cs => Books/Model/Author.cs} (74%) rename src/NzbDrone.Core/{Music/Model/ArtistMetadata.cs => Books/Model/AuthorMetadata.cs} (66%) rename src/NzbDrone.Core/{Music/Model/Album.cs => Books/Model/Book.cs} (59%) rename src/NzbDrone.Core/{Music => Books}/Model/Entity.cs (100%) rename src/NzbDrone.Core/{Music => Books}/Model/Links.cs (100%) rename src/NzbDrone.Core/{Music => Books}/Model/MonitoringOptions.cs (100%) rename src/NzbDrone.Core/{Music => Books}/Model/Ratings.cs (100%) create mode 100644 src/NzbDrone.Core/Books/Model/Series.cs create mode 100644 src/NzbDrone.Core/Books/Model/SeriesBookLink.cs create mode 100644 src/NzbDrone.Core/Books/Repositories/AlbumRepository.cs rename src/NzbDrone.Core/{Music => Books}/Repositories/ArtistMetadataRepository.cs (70%) create mode 100644 src/NzbDrone.Core/Books/Repositories/ArtistRepository.cs create mode 100644 src/NzbDrone.Core/Books/Repositories/SeriesBookLinkRepository.cs create mode 100644 src/NzbDrone.Core/Books/Repositories/SeriesRepository.cs rename src/NzbDrone.Core/{Music => Books}/Services/AddAlbumService.cs (61%) rename src/NzbDrone.Core/{Music/Services/AddArtistService.cs => Books/Services/AddAuthorService.cs} (83%) rename src/NzbDrone.Core/{Music => Books}/Services/AlbumAddedService.cs (90%) rename src/NzbDrone.Core/{Music => Books}/Services/AlbumCutoffService.cs (89%) rename src/NzbDrone.Core/{Music => Books}/Services/AlbumMonitoredService.cs (93%) rename src/NzbDrone.Core/{Music => Books}/Services/AlbumService.cs (57%) rename src/NzbDrone.Core/{Music => Books}/Services/ArtistMetadataService.cs (72%) rename src/NzbDrone.Core/{Music => Books}/Services/ArtistService.cs (62%) rename src/NzbDrone.Core/{Music => Books}/Services/MoveArtistService.cs (93%) rename src/NzbDrone.Core/{Music/Services/RefreshArtistService.cs => Books/Services/RefreshAuthorService.cs} (66%) create mode 100644 src/NzbDrone.Core/Books/Services/RefreshBookService.cs rename src/NzbDrone.Core/{Music => Books}/Services/RefreshEntityServiceBase.cs (84%) create mode 100644 src/NzbDrone.Core/Books/Services/RefreshSeriesBookLinkService.cs create mode 100644 src/NzbDrone.Core/Books/Services/RefreshSeriesService.cs create mode 100644 src/NzbDrone.Core/Books/Services/SeriesBookLinkService.cs create mode 100644 src/NzbDrone.Core/Books/Services/SeriesService.cs rename src/NzbDrone.Core/{Music => Books}/Utilities/AddArtistValidator.cs (91%) rename src/NzbDrone.Core/{Music => Books}/Utilities/ArtistPathBuilder.cs (87%) rename src/NzbDrone.Core/{Music => Books}/Utilities/ShouldRefreshAlbum.cs (93%) rename src/NzbDrone.Core/{Music => Books}/Utilities/ShouldRefreshArtist.cs (95%) create mode 100644 src/NzbDrone.Core/Books/output.txt delete mode 100644 src/NzbDrone.Core/Datastore/Converters/PrimaryAlbumTypeIntConverter.cs delete mode 100644 src/NzbDrone.Core/Datastore/Converters/ReleaseStatusIntConverter.cs delete mode 100644 src/NzbDrone.Core/Datastore/Converters/SecondaryAlbumTypeIntConverter.cs delete mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksGrabSpecification.cs delete mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksSpecification.cs delete mode 100644 src/NzbDrone.Core/Extras/Lyrics/ExistingLyricImporter.cs delete mode 100644 src/NzbDrone.Core/Extras/Lyrics/ImportedLyricFiles.cs delete mode 100644 src/NzbDrone.Core/Extras/Lyrics/LyricFile.cs delete mode 100644 src/NzbDrone.Core/Extras/Lyrics/LyricFileExtensions.cs delete mode 100644 src/NzbDrone.Core/Extras/Lyrics/LyricFileRepository.cs delete mode 100644 src/NzbDrone.Core/Extras/Lyrics/LyricFileService.cs delete mode 100644 src/NzbDrone.Core/Extras/Lyrics/LyricService.cs delete mode 100644 src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs delete mode 100644 src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs delete mode 100644 src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs delete mode 100644 src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs delete mode 100644 src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs delete mode 100644 src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs delete mode 100644 src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs delete mode 100644 src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs delete mode 100644 src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs create mode 100644 src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsBookshelf.cs create mode 100644 src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsBookshelfSettings.cs create mode 100644 src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsException.cs create mode 100644 src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsImportListBase.cs create mode 100644 src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsOwnedBooks.cs create mode 100644 src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsSettingsBase.cs delete mode 100644 src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportApi.cs delete mode 100644 src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportRequestGenerator.cs delete mode 100644 src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportSettings.cs delete mode 100644 src/NzbDrone.Core/ImportLists/LastFm/LastFmApi.cs delete mode 100644 src/NzbDrone.Core/ImportLists/LastFm/LastFmParser.cs delete mode 100644 src/NzbDrone.Core/ImportLists/LastFm/LastFmTag.cs delete mode 100644 src/NzbDrone.Core/ImportLists/LastFm/LastFmTagSettings.cs delete mode 100644 src/NzbDrone.Core/ImportLists/LastFm/LastFmUser.cs delete mode 100644 src/NzbDrone.Core/ImportLists/LastFm/LastFmUserRequestGenerator.cs delete mode 100644 src/NzbDrone.Core/ImportLists/LastFm/LastFmUserSettings.cs rename src/NzbDrone.Core/ImportLists/{HeadphonesImport/HeadphonesImport.cs => LazyLibrarian/LazyLibrarianImport.cs} (50%) create mode 100644 src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportApi.cs rename src/NzbDrone.Core/ImportLists/{HeadphonesImport/HeadphonesImportParser.cs => LazyLibrarian/LazyLibrarianImportParser.cs} (85%) rename src/NzbDrone.Core/ImportLists/{LastFm/LastFmTagRequestGenerator.cs => LazyLibrarian/LazyLibrarianImportRequestGenerator.cs} (64%) create mode 100644 src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportSettings.cs delete mode 100644 src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrLists.cs delete mode 100644 src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsApi.cs delete mode 100644 src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsParser.cs delete mode 100644 src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsRequestGenerator.cs delete mode 100644 src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsSettings.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyException.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyFollowedArtists.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListItemInfo.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyMap.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylistSettings.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyProxy.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs delete mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifySettingsBase.cs delete mode 100644 src/NzbDrone.Core/Indexers/Headphones/Headphones.cs delete mode 100644 src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilities.cs delete mode 100644 src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilitiesProvider.cs delete mode 100644 src/NzbDrone.Core/Indexers/Headphones/HeadphonesRequestGenerator.cs delete mode 100644 src/NzbDrone.Core/Indexers/Headphones/HeadphonesRssParser.cs delete mode 100644 src/NzbDrone.Core/Indexers/Headphones/HeadphonesSettings.cs delete mode 100644 src/NzbDrone.Core/Indexers/Waffles/Waffles.cs delete mode 100644 src/NzbDrone.Core/Indexers/Waffles/WafflesRequestGenerator.cs delete mode 100644 src/NzbDrone.Core/Indexers/Waffles/WafflesRssParser.cs delete mode 100644 src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs create mode 100644 src/NzbDrone.Core/MediaFiles/AzwTag/Azw3File.cs create mode 100644 src/NzbDrone.Core/MediaFiles/AzwTag/Headers.cs create mode 100644 src/NzbDrone.Core/MediaFiles/AzwTag/ProcessSection.cs create mode 100644 src/NzbDrone.Core/MediaFiles/AzwTag/Structs.cs create mode 100644 src/NzbDrone.Core/MediaFiles/AzwTag/Utils.cs rename src/NzbDrone.Core/MediaFiles/{TrackFile.cs => BookFile.cs} (82%) create mode 100644 src/NzbDrone.Core/MediaFiles/EbookTagService.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Entities/EpubBook.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Entities/EpubSchema.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/EpubReader.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Readers/PackageReader.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Readers/RootFilePathReader.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Readers/SchemaReader.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/RefEntities/EpubBookRef.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Common/ManifestProperty.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Common/StructuralSemanticsProperty.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadata.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataContributor.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataCreator.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataDate.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataIdentifier.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataMeta.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubPackage.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubVersion.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/PageProgressionDirection.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Utils/StringExtensionMethods.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Utils/VersionUtils.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Utils/XmlExtensionMethods.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Utils/XmlUtils.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpubTag/Utils/ZipPathUtils.cs create mode 100644 src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateCalibreData.cs delete mode 100644 src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalcualtor.cs create mode 100644 src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs delete mode 100644 src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseTrackMatchSpecification.cs delete mode 100644 src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs delete mode 100644 src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs delete mode 100644 src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/ReleaseWantedSpecification.cs delete mode 100644 src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameTracksImportSpecification.cs rename src/NzbDrone.Core/MetadataSource/{IProvideArtistInfo.cs => IProvideAuthorInfo.cs} (63%) create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Extensions/HttpResponseExtensions.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Extensions/XmlExtensions.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorBookListResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorSeriesListResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorSummaryResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BestBookResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookLinkResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookSearchResultResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookSummaryResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/GoodreadsResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/OwnedBookResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/PaginatedList.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/PaginationModel.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/ReviewResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/SeriesResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/UserShelfResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/WorkResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EntityResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/LinkResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MediumResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RatingResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RecentUpdatesResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ReleaseResource.cs delete mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/AuthorResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/AuthorSummaryResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BookResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BookSearchResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BulkResourceBase.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/ContributorResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/SeriesResource.cs delete mode 100644 src/NzbDrone.Core/Music/Events/AlbumAddedEvent.cs delete mode 100644 src/NzbDrone.Core/Music/Events/AlbumInfoRefreshedEvent.cs delete mode 100644 src/NzbDrone.Core/Music/Events/ArtistsImportedEvent.cs delete mode 100644 src/NzbDrone.Core/Music/Events/ReleaseDeletedEvent.cs delete mode 100644 src/NzbDrone.Core/Music/Model/Medium.cs delete mode 100644 src/NzbDrone.Core/Music/Model/Member.cs delete mode 100644 src/NzbDrone.Core/Music/Model/PrimaryAlbumType.cs delete mode 100644 src/NzbDrone.Core/Music/Model/Release.cs delete mode 100644 src/NzbDrone.Core/Music/Model/ReleaseStatus.cs delete mode 100644 src/NzbDrone.Core/Music/Model/SecondaryAlbumType.cs delete mode 100644 src/NzbDrone.Core/Music/Model/Track.cs delete mode 100644 src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs delete mode 100644 src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs delete mode 100644 src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs delete mode 100644 src/NzbDrone.Core/Music/Repositories/TrackRepository.cs delete mode 100644 src/NzbDrone.Core/Music/Services/AlbumEditedService.cs delete mode 100644 src/NzbDrone.Core/Music/Services/RefreshAlbumReleaseService.cs delete mode 100644 src/NzbDrone.Core/Music/Services/RefreshAlbumService.cs delete mode 100644 src/NzbDrone.Core/Music/Services/RefreshTrackService.cs delete mode 100644 src/NzbDrone.Core/Music/Services/ReleaseService.cs delete mode 100644 src/NzbDrone.Core/Music/Services/TrackService.cs delete mode 100644 src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs delete mode 100644 src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs delete mode 100644 src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs delete mode 100644 src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs delete mode 100644 src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaFolder.cs delete mode 100644 src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaFoldersResponse.cs delete mode 100644 src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaUpdateInfo.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexAuthenticationException.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexException.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinResponse.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinUrlResponse.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvSignInUrlResponse.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexVersionException.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexError.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexIdentity.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexPreferences.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexResponse.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexSection.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexSectionItem.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs delete mode 100644 src/NzbDrone.Core/Notifications/Webhook/WebhookTrack.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/Model/ActivePlayer.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/Model/ActivePlayersResult.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/Model/ArtistResponse.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/Model/ArtistResult.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/Model/ErrorResult.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/Model/KodiArtist.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/Model/XbmcJsonResult.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonException.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs delete mode 100644 src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs delete mode 100644 src/NzbDrone.Core/Profiles/Metadata/ProfilePrimaryAlbumTypeItem.cs delete mode 100644 src/NzbDrone.Core/Profiles/Metadata/ProfileReleaseStatusTypeItem.cs delete mode 100644 src/NzbDrone.Core/Profiles/Metadata/ProfileSecondaryAlbumTypeItem.cs delete mode 100644 src/NzbDrone.Host/NzbDrone.ico create mode 100644 src/NzbDrone.Host/Readarr.ico delete mode 100644 src/NzbDrone.Integration.Test/ApiTests/TrackFixture.cs delete mode 100644 src/NzbDrone.Integration.Test/Client/TrackClient.cs delete mode 100644 src/Readarr.Api.V1/Albums/AlbumReleaseResource.cs delete mode 100644 src/Readarr.Api.V1/Albums/MediumResource.cs rename src/Readarr.Api.V1/{Tracks/RenameTrackModule.cs => Albums/RenameBookModule.cs} (62%) rename src/Readarr.Api.V1/{Tracks/RenameTrackResource.cs => Albums/RenameBookResource.cs} (76%) rename src/Readarr.Api.V1/{Tracks/RetagTrackModule.cs => Albums/RetagBookModule.cs} (63%) rename src/Readarr.Api.V1/{Tracks/RetagTrackResource.cs => Albums/RetagBookResource.cs} (88%) delete mode 100644 src/Readarr.Api.V1/Profiles/Metadata/MetadataValidator.cs create mode 100644 src/Readarr.Api.V1/Series/SeriesBookLinkResource.cs create mode 100644 src/Readarr.Api.V1/Series/SeriesModule.cs create mode 100644 src/Readarr.Api.V1/Series/SeriesResource.cs delete mode 100644 src/Readarr.Api.V1/Tracks/TrackModule.cs delete mode 100644 src/Readarr.Api.V1/Tracks/TrackModuleWithSignalR.cs delete mode 100644 src/Readarr.Api.V1/Tracks/TrackResource.cs diff --git a/Logo/1024.png b/Logo/1024.png index f048bf77dc248651974ef15948f039c305753a81..a66d4e23e7284e385382d5f6ed3d060111ae4f60 100644 GIT binary patch literal 39632 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 Eg5H9WLjV8( literal 52098 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 delta 541 zcmV+&0^lq&<>lq&<>lt(<>lq&(%jee>+tmJ@a5&@ z`tSAR<>eL$4*(kr2mk>9l~eNc^YHTV10N9$0R;j80F%J0eW#OnqK}4#hC4euDk&)? zCMEz70(zv8fRu{>LM89;@ZkCG<>lqp<=d&uyuY-$l*+D_!GEo$xUB$^Qu_S-K!amM zdSOCuTL6$yIb=@(f=D4%Jr7be08J?M@AC{Q90w&92^td{4HFC$5Dp9t02T@g2nY*3 zCy~6T|M~v@_4@Aa?(OaEoW`+2ot3~D+6007%bL_t(| zUNw)^cEd0b1tm*%+KS9fVP>W>GyVU!)Q;16TRM9O!Fm;$;F?O};&AaJR41bWu!>bWXoAYw_n`;Qb3+C0u zSRH;w#%$AqKz`abCO>MYqn?Yt^(X^5+4s$_Lu1m|CUSPNnepgjsE)0=XW0-SeGzoa z^JdBhm%&0u08>}^*VWoCs`zG1aUQI<4)5EmL9l)$QV~7oCZAeMmni>on*H$H(sH?i fGXzT~M;84*YO5J6J7INS00000NkvXXu0mjfYdZjK diff --git a/Logo/256.png b/Logo/256.png index 10bff98c09f779b698e363c0febf70a685fd3375..8ad8017302aadf72a75ed662fa123c164f4782ac 100644 GIT binary patch literal 11658 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�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 S;nnk(7C>HF1zsj;68Jwhw@VfP diff --git a/Logo/32.png b/Logo/32.png index 661067300ebe2cd7b5a35c75d5ff153aac2ce793..f60ea9533014ad0b20584a0eb7802a663754d20c 100644 GIT binary patch delta 1266 zcmVgwz3>*M0$-`?KS($ehg?BwL+=;-L-;NaTX z+04qyy}P^R<>lq&<>=_>=H=z)<>lJh*~G%a!@UUSzR<6)iiL%SfPjv1aj|S{h-zwYX=$xwWJf=ilGm+<)BJ*x2LN*44ki(YLqAp`o^ScW-EDuV-h4W@fBmVSiv?Us+kER#tdc zR$Nt8qfSm@OG}?eM^QsVTs=KZJ3CG~I+80Z=H}+) zIh8RnihnOJel0C-EiK*_ma_l=048)&PE!E)?Y(^_^5fBxE+Yr>@YB7xN+2Bk*VI~A zEBN7+hJ}MyED48)EWy_50007zNkl=5XQGI6xu>@hr8@HO=@YWLXDOy z^x$xJcXyY=Id^yYmX{Qo9<;oFvX==6?mOilrG6o+vR<#LLYrl7Sdy<%zin zhK0E4cM@(K3-}yZM(VVh%2th5M~y)a{~D5_6e7~DHyG78t~MI<0RjmXs_1#L7}fh7 z&fSV-Yhz>iQboVhV1JC#lG*$6RLUGWvdz?%LRzJ@KHG6fL!_puuIeih*^38#@IN)S z>ya;QEsM?54Ud{So!^7A zwWitmm+#s>4+qBNMIeJq>W6+~m}T^^Z+~rivNLFEYT5faYQeCRAw9({1QMikd>p{8 zRJFa`5sACE3_6qxcu;G6(;Z1#&i+^;amR*H3wb~UHR^>OETZ!oHf(EFYoG`u!dBe+ zfz6ggfqUh6B}}>mI&J~p(*RRqzA6e-M**fpPPoa9_`oZsNoGvL)=oU2DoBqh0e_jy zkNUx8?>}I}hX;C+n+c@pWr2~Bc);1PeOxRBvI0m9p2q|JIw+W53_t=0`A@4aZUPrI zrz?mf2ro=_of+?0Slb_E3ZKxQUwi9|J-E64a$$f#frwtb-DjHHw_slnF-zZ^si__B zqL5Mm!cwI{!W=rb`P}6L-2;7A_iK#pC}bv6#VPFCr=vQ*!P#H2w7TB1x=gRV!#{!K z$$*tgk&TEz#5fMUokBuoN{sa=#(I^s^~iDBdIE4=2?g?~dByozAVxB2A#MMpq#T|k cLmIXJKkIO}`KdQ--T(jq07*qoM6N<$g4s5jX#fBK delta 1347 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 a^6S4x#z+@-{Kgsp0000>+w diff --git a/Logo/Readarr.svg b/Logo/Readarr.svg index 9e7fb48db..b8ec609ba 100644 --- a/Logo/Readarr.svg +++ b/Logo/Readarr.svg @@ -1,25 +1,23 @@ - - - - - - - - - - - - - {albumType} - - - } - } @@ -483,7 +397,7 @@ class AlbumDetails extends Component {

@@ -492,90 +406,92 @@ class AlbumDetails extends Component {
{ - !isPopulated && !albumsError && !trackFilesError && + !isPopulated && !trackFilesError && } - { - !isFetching && albumsError && -
Loading albums failed
- } - { !isFetching && trackFilesError && -
Loading track files failed
- } - - { - isPopulated && !!media.length && -
- - { - media.slice(0).map((medium) => { - return ( - - ); - }) - } -
+
Loading book files failed
} + this.setState({ selectedTabIndex: tabIndex })}> + + + History + + + + Search + + + + Files + + + { + selectedTabIndex === 1 && +
+ +
+ } + +
+ + + + + + + + + + + + +
- - - - - - - - - + {/* */} @@ -587,26 +503,22 @@ class AlbumDetails extends Component { AlbumDetails.propTypes = { id: PropTypes.number.isRequired, - foreignAlbumId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, title: PropTypes.string.isRequired, disambiguation: PropTypes.string, duration: PropTypes.number, overview: PropTypes.string, - albumType: PropTypes.string.isRequired, statistics: PropTypes.object.isRequired, releaseDate: PropTypes.string.isRequired, ratings: PropTypes.object.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, links: PropTypes.arrayOf(PropTypes.object).isRequired, - media: PropTypes.arrayOf(PropTypes.object).isRequired, monitored: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, isSaving: PropTypes.bool.isRequired, isSearching: PropTypes.bool, isFetching: PropTypes.bool, isPopulated: PropTypes.bool, - albumsError: PropTypes.object, - tracksError: PropTypes.object, trackFilesError: PropTypes.object, hasTrackFiles: PropTypes.bool.isRequired, artist: PropTypes.object, diff --git a/frontend/src/Album/Details/AlbumDetailsConnector.js b/frontend/src/Album/Details/AlbumDetailsConnector.js index 12a7c94b7..462ae778b 100644 --- a/frontend/src/Album/Details/AlbumDetailsConnector.js +++ b/frontend/src/Album/Details/AlbumDetailsConnector.js @@ -8,8 +8,8 @@ import { findCommand, isCommandExecuting } from 'Utilities/Command'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import { toggleAlbumsMonitored } from 'Store/Actions/albumActions'; -import { fetchTracks, clearTracks } from 'Store/Actions/trackActions'; import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions'; +import { clearReleases, cancelFetchReleases } from 'Store/Actions/releaseActions'; import { executeCommand } from 'Store/Actions/commandActions'; import * as commandNames from 'Commands/commandNames'; import AlbumDetails from './AlbumDetails'; @@ -39,18 +39,17 @@ const selectTrackFiles = createSelector( function createMapStateToProps() { return createSelector( - (state, { foreignAlbumId }) => foreignAlbumId, - (state) => state.tracks, + (state, { titleSlug }) => titleSlug, selectTrackFiles, (state) => state.albums, createAllArtistSelector(), createCommandsSelector(), createUISettingsSelector(), - (foreignAlbumId, tracks, trackFiles, albums, artists, commands, uiSettings) => { + (titleSlug, trackFiles, albums, artists, commands, uiSettings) => { const sortedAlbums = _.orderBy(albums.items, 'releaseDate'); - const albumIndex = _.findIndex(sortedAlbums, { foreignAlbumId }); + const albumIndex = _.findIndex(sortedAlbums, { titleSlug }); const album = sortedAlbums[albumIndex]; - const artist = _.find(artists, { id: album.artistId }); + const artist = _.find(artists, { id: album.authorId }); if (!album) { return {}; @@ -68,12 +67,11 @@ function createMapStateToProps() { const isSearchingCommand = findCommand(commands, { name: commandNames.ALBUM_SEARCH }); const isSearching = ( isCommandExecuting(isSearchingCommand) && - isSearchingCommand.body.albumIds.indexOf(album.id) > -1 + isSearchingCommand.body.bookIds.indexOf(album.id) > -1 ); - const isFetching = tracks.isFetching || isTrackFilesFetching; - const isPopulated = tracks.isPopulated && isTrackFilesPopulated; - const tracksError = tracks.error; + const isFetching = isTrackFilesFetching; + const isPopulated = isTrackFilesPopulated; return { ...album, @@ -82,7 +80,6 @@ function createMapStateToProps() { isSearching, isFetching, isPopulated, - tracksError, trackFilesError, hasTrackFiles, previousAlbum, @@ -94,17 +91,13 @@ function createMapStateToProps() { const mapDispatchToProps = { executeCommand, - fetchTracks, - clearTracks, fetchTrackFiles, clearTrackFiles, + clearReleases, + cancelFetchReleases, toggleAlbumsMonitored }; -function getMonitoredReleases(props) { - return _.map(_.filter(props.releases, { monitored: true }), 'id').sort(); -} - class AlbumDetailsConnector extends Component { componentDidMount() { @@ -113,8 +106,10 @@ class AlbumDetailsConnector extends Component { } componentDidUpdate(prevProps) { - if (!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) || - (prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) { + // If the id has changed we need to clear the albums + // files and fetch from the server. + + if (prevProps.id !== this.props.id) { this.unpopulate(); this.populate(); } @@ -129,14 +124,14 @@ class AlbumDetailsConnector extends Component { // Control populate = () => { - const albumId = this.props.id; + const bookId = this.props.id; - this.props.fetchTracks({ albumId }); - this.props.fetchTrackFiles({ albumId }); + this.props.fetchTrackFiles({ bookId }); } unpopulate = () => { - this.props.clearTracks(); + this.props.cancelFetchReleases(); + this.props.clearReleases(); this.props.clearTrackFiles(); } @@ -145,7 +140,7 @@ class AlbumDetailsConnector extends Component { onMonitorTogglePress = (monitored) => { this.props.toggleAlbumsMonitored({ - albumIds: [this.props.id], + bookIds: [this.props.id], monitored }); } @@ -153,7 +148,7 @@ class AlbumDetailsConnector extends Component { onSearchPress = () => { this.props.executeCommand({ name: commandNames.ALBUM_SEARCH, - albumIds: [this.props.id] + bookIds: [this.props.id] }); } @@ -176,11 +171,11 @@ AlbumDetailsConnector.propTypes = { anyReleaseOk: PropTypes.bool, isAlbumFetching: PropTypes.bool, isAlbumPopulated: PropTypes.bool, - foreignAlbumId: PropTypes.string.isRequired, - fetchTracks: PropTypes.func.isRequired, - clearTracks: PropTypes.func.isRequired, + titleSlug: PropTypes.string.isRequired, fetchTrackFiles: PropTypes.func.isRequired, clearTrackFiles: PropTypes.func.isRequired, + clearReleases: PropTypes.func.isRequired, + cancelFetchReleases: PropTypes.func.isRequired, toggleAlbumsMonitored: PropTypes.func.isRequired, executeCommand: PropTypes.func.isRequired }; diff --git a/frontend/src/Album/Details/AlbumDetailsLinks.js b/frontend/src/Album/Details/AlbumDetailsLinks.js index 265a7c4ff..830173ace 100644 --- a/frontend/src/Album/Details/AlbumDetailsLinks.js +++ b/frontend/src/Album/Details/AlbumDetailsLinks.js @@ -7,26 +7,12 @@ import styles from './AlbumDetailsLinks.css'; function AlbumDetailsLinks(props) { const { - foreignAlbumId, links } = props; return (
- - - - {links.map((link, index) => { return ( @@ -56,7 +42,6 @@ function AlbumDetailsLinks(props) { } AlbumDetailsLinks.propTypes = { - foreignAlbumId: PropTypes.string.isRequired, links: PropTypes.arrayOf(PropTypes.object).isRequired }; diff --git a/frontend/src/Album/Details/AlbumDetailsMedium.js b/frontend/src/Album/Details/AlbumDetailsMedium.js deleted file mode 100644 index 33d6efb80..000000000 --- a/frontend/src/Album/Details/AlbumDetailsMedium.js +++ /dev/null @@ -1,210 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { icons, kinds, sizes } from 'Helpers/Props'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import Label from 'Components/Label'; -import Link from 'Components/Link/Link'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TrackRowConnector from './TrackRowConnector'; -import styles from './AlbumDetailsMedium.css'; - -function getMediumStatistics(tracks) { - let trackCount = 0; - let trackFileCount = 0; - let totalTrackCount = 0; - - tracks.forEach((track) => { - if (track.trackFileId) { - trackCount++; - trackFileCount++; - } else { - trackCount++; - } - - totalTrackCount++; - }); - - return { - trackCount, - trackFileCount, - totalTrackCount - }; -} - -function getTrackCountKind(monitored, trackFileCount, trackCount) { - if (trackFileCount === trackCount && trackCount > 0) { - return kinds.SUCCESS; - } - - if (!monitored) { - return kinds.WARNING; - } - - return kinds.DANGER; -} - -class AlbumDetailsMedium extends Component { - - // - // Lifecycle - - componentDidMount() { - this._expandByDefault(); - } - - componentDidUpdate(prevProps) { - if (prevProps.albumId !== this.props.albumId) { - this._expandByDefault(); - } - } - - // - // Control - - _expandByDefault() { - const { - mediumNumber, - onExpandPress - } = this.props; - - onExpandPress(mediumNumber, mediumNumber === 1); - } - - // - // Listeners - - onExpandPress = () => { - const { - mediumNumber, - isExpanded - } = this.props; - - this.props.onExpandPress(mediumNumber, !isExpanded); - } - - // - // Render - - render() { - const { - mediumNumber, - mediumFormat, - albumMonitored, - items, - columns, - onTableOptionChange, - isExpanded, - isSmallScreen - } = this.props; - - const { - trackCount, - trackFileCount, - totalTrackCount - } = getMediumStatistics(items); - - return ( -
-
-
- { -
- - {mediumFormat} {mediumNumber} - -
- } - - -
- - - - { - !isSmallScreen && -   - } - - -
- -
- { - isExpanded && -
- { - items.length ? - - - { - items.map((item) => { - return ( - - ); - }) - } - -
: - -
- No tracks in this medium -
- } -
- -
-
- } -
-
- ); - } -} - -AlbumDetailsMedium.propTypes = { - albumId: PropTypes.number.isRequired, - albumMonitored: PropTypes.bool.isRequired, - mediumNumber: PropTypes.number.isRequired, - mediumFormat: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - isSaving: PropTypes.bool, - isExpanded: PropTypes.bool, - isSmallScreen: PropTypes.bool.isRequired, - onTableOptionChange: PropTypes.func.isRequired, - onExpandPress: PropTypes.func.isRequired -}; - -export default AlbumDetailsMedium; diff --git a/frontend/src/Album/Details/AlbumDetailsMediumConnector.js b/frontend/src/Album/Details/AlbumDetailsMediumConnector.js deleted file mode 100644 index e05d9870d..000000000 --- a/frontend/src/Album/Details/AlbumDetailsMediumConnector.js +++ /dev/null @@ -1,65 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; -import { setTracksTableOption } from 'Store/Actions/trackActions'; -import { executeCommand } from 'Store/Actions/commandActions'; -import AlbumDetailsMedium from './AlbumDetailsMedium'; - -function createMapStateToProps() { - return createSelector( - (state, { mediumNumber }) => mediumNumber, - (state) => state.tracks, - createDimensionsSelector(), - (mediumNumber, tracks, dimensions) => { - - const tracksInMedium = _.filter(tracks.items, { mediumNumber }); - const sortedTracks = _.orderBy(tracksInMedium, ['absoluteTrackNumber'], ['asc']); - - return { - items: sortedTracks, - columns: tracks.columns, - isSmallScreen: dimensions.isSmallScreen - }; - } - ); -} - -const mapDispatchToProps = { - setTracksTableOption, - executeCommand -}; - -class AlbumDetailsMediumConnector extends Component { - - // - // Listeners - - onTableOptionChange = (payload) => { - this.props.setTracksTableOption(payload); - } - - // - // Render - - render() { - return ( - - ); - } -} - -AlbumDetailsMediumConnector.propTypes = { - albumId: PropTypes.number.isRequired, - albumMonitored: PropTypes.bool.isRequired, - mediumNumber: PropTypes.number.isRequired, - setTracksTableOption: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsMediumConnector); diff --git a/frontend/src/Album/Details/AlbumDetailsPageConnector.js b/frontend/src/Album/Details/AlbumDetailsPageConnector.js index 320348b9b..232c84acd 100644 --- a/frontend/src/Album/Details/AlbumDetailsPageConnector.js +++ b/frontend/src/Album/Details/AlbumDetailsPageConnector.js @@ -17,14 +17,14 @@ function createMapStateToProps() { (state) => state.albums, (state) => state.artist, (match, albums, artist) => { - const foreignAlbumId = match.params.foreignAlbumId; + const titleSlug = match.params.titleSlug; const isFetching = albums.isFetching || artist.isFetching; const isPopulated = albums.isPopulated && artist.isPopulated; // if albums have been fetched, make sure requested one exists - // otherwise don't map foreignAlbumId to trigger not found page + // otherwise don't map titleSlug to trigger not found page if (!isFetching && isPopulated) { - const albumIndex = _.findIndex(albums.items, { foreignAlbumId }); + const albumIndex = _.findIndex(albums.items, { titleSlug }); if (albumIndex === -1) { return { isFetching, @@ -34,7 +34,7 @@ function createMapStateToProps() { } return { - foreignAlbumId, + titleSlug, isFetching, isPopulated }; @@ -69,10 +69,10 @@ class AlbumDetailsPageConnector extends Component { // Control populate = () => { - const foreignAlbumId = this.props.foreignAlbumId; + const titleSlug = this.props.titleSlug; this.setState({ hasMounted: true }); this.props.fetchAlbums({ - foreignAlbumId, + titleSlug, includeAllArtistAlbums: true }); } @@ -86,15 +86,15 @@ class AlbumDetailsPageConnector extends Component { render() { const { - foreignAlbumId, + titleSlug, isFetching, isPopulated } = this.props; - if (!foreignAlbumId) { + if (!titleSlug) { return ( ); } @@ -113,7 +113,7 @@ class AlbumDetailsPageConnector extends Component { if (!isFetching && isPopulated && this.state.hasMounted) { return ( ); } @@ -121,8 +121,8 @@ class AlbumDetailsPageConnector extends Component { } AlbumDetailsPageConnector.propTypes = { - foreignAlbumId: PropTypes.string, - match: PropTypes.shape({ params: PropTypes.shape({ foreignAlbumId: PropTypes.string.isRequired }).isRequired }).isRequired, + titleSlug: PropTypes.string, + match: PropTypes.shape({ params: PropTypes.shape({ titleSlug: PropTypes.string.isRequired }).isRequired }).isRequired, push: PropTypes.func.isRequired, fetchAlbums: PropTypes.func.isRequired, clearAlbums: PropTypes.func.isRequired, diff --git a/frontend/src/Album/Details/TrackActionsCell.css b/frontend/src/Album/Details/TrackActionsCell.css deleted file mode 100644 index 6b80ba0e0..000000000 --- a/frontend/src/Album/Details/TrackActionsCell.css +++ /dev/null @@ -1,6 +0,0 @@ -.TrackActionsCell { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 70px; - white-space: nowrap; -} diff --git a/frontend/src/Album/Details/TrackActionsCell.js b/frontend/src/Album/Details/TrackActionsCell.js deleted file mode 100644 index db73b35b7..000000000 --- a/frontend/src/Album/Details/TrackActionsCell.js +++ /dev/null @@ -1,109 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { icons, kinds } from 'Helpers/Props'; -import IconButton from 'Components/Link/IconButton'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import FileDetailsModal from 'TrackFile/FileDetailsModal'; -import styles from './TrackActionsCell.css'; - -class TrackActionsCell extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isDetailsModalOpen: false, - isConfirmDeleteModalOpen: false - }; - } - - // - // Listeners - - onDetailsPress = () => { - this.setState({ isDetailsModalOpen: true }); - } - - onDetailsModalClose = () => { - this.setState({ isDetailsModalOpen: false }); - } - - onDeleteFilePress = () => { - this.setState({ isConfirmDeleteModalOpen: true }); - } - - onConfirmDelete = () => { - this.setState({ isConfirmDeleteModalOpen: false }); - this.props.deleteTrackFile({ id: this.props.trackFileId }); - } - - onConfirmDeleteModalClose = () => { - this.setState({ isConfirmDeleteModalOpen: false }); - } - - // - // Render - - render() { - - const { - trackFileId, - trackFilePath - } = this.props; - - const { - isDetailsModalOpen, - isConfirmDeleteModalOpen - } = this.state; - - return ( - - { - trackFilePath && - - } - { - trackFilePath && - - } - - - - - - - ); - } -} - -TrackActionsCell.propTypes = { - id: PropTypes.number.isRequired, - albumId: PropTypes.number.isRequired, - trackFilePath: PropTypes.string, - trackFileId: PropTypes.number.isRequired, - deleteTrackFile: PropTypes.func.isRequired -}; - -export default TrackActionsCell; diff --git a/frontend/src/Album/Details/TrackRow.css b/frontend/src/Album/Details/TrackRow.css deleted file mode 100644 index c77d215f2..000000000 --- a/frontend/src/Album/Details/TrackRow.css +++ /dev/null @@ -1,30 +0,0 @@ -.title { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - white-space: nowrap; -} - -.monitored { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 42px; -} - -.trackNumber { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 50px; -} - -.audio { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 250px; -} - -.duration, -.status { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 100px; -} diff --git a/frontend/src/Album/Details/TrackRow.js b/frontend/src/Album/Details/TrackRow.js deleted file mode 100644 index f4d26ac6f..000000000 --- a/frontend/src/Album/Details/TrackRow.js +++ /dev/null @@ -1,166 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import TableRow from 'Components/Table/TableRow'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import EpisodeStatusConnector from 'Album/EpisodeStatusConnector'; -import MediaInfoConnector from 'TrackFile/MediaInfoConnector'; -import TrackActionsCell from './TrackActionsCell'; -import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes'; - -import styles from './TrackRow.css'; - -class TrackRow extends Component { - - // - // Render - - render() { - const { - id, - albumId, - mediumNumber, - trackFileId, - absoluteTrackNumber, - title, - duration, - trackFilePath, - columns, - deleteTrackFile - } = this.props; - - return ( - - { - columns.map((column) => { - const { - name, - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (name === 'medium') { - return ( - - {mediumNumber} - - ); - } - - if (name === 'absoluteTrackNumber') { - return ( - - {absoluteTrackNumber} - - ); - } - - if (name === 'title') { - return ( - - {title} - - ); - } - - if (name === 'path') { - return ( - - { - trackFilePath - } - - ); - } - - if (name === 'duration') { - return ( - - { - formatTimeSpan(duration) - } - - ); - } - - if (name === 'audioInfo') { - return ( - - - - ); - } - - if (name === 'status') { - return ( - - - - ); - } - - if (name === 'actions') { - return ( - - ); - } - - return null; - }) - } - - ); - } -} - -TrackRow.propTypes = { - deleteTrackFile: PropTypes.func.isRequired, - id: PropTypes.number.isRequired, - albumId: PropTypes.number.isRequired, - trackFileId: PropTypes.number, - mediumNumber: PropTypes.number.isRequired, - trackNumber: PropTypes.string.isRequired, - absoluteTrackNumber: PropTypes.number, - title: PropTypes.string.isRequired, - duration: PropTypes.number.isRequired, - isSaving: PropTypes.bool, - trackFilePath: PropTypes.string, - mediaInfo: PropTypes.object, - columns: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default TrackRow; diff --git a/frontend/src/Album/Details/TrackRowConnector.js b/frontend/src/Album/Details/TrackRowConnector.js deleted file mode 100644 index 7b5e7f7b9..000000000 --- a/frontend/src/Album/Details/TrackRowConnector.js +++ /dev/null @@ -1,23 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector'; -import { deleteTrackFile } from 'Store/Actions/trackFileActions'; -import TrackRow from './TrackRow'; - -function createMapStateToProps() { - return createSelector( - (state, { id }) => id, - createTrackFileSelector(), - (id, trackFile) => { - return { - trackFilePath: trackFile ? trackFile.path : null - }; - } - ); -} - -const mapDispatchToProps = { - deleteTrackFile -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(TrackRow); diff --git a/frontend/src/Album/Edit/EditAlbumModal.js b/frontend/src/Album/Edit/EditAlbumModal.js deleted file mode 100644 index d47bb284f..000000000 --- a/frontend/src/Album/Edit/EditAlbumModal.js +++ /dev/null @@ -1,25 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import EditAlbumModalContentConnector from './EditAlbumModalContentConnector'; - -function EditAlbumModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditAlbumModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditAlbumModal; diff --git a/frontend/src/Album/Edit/EditAlbumModalConnector.js b/frontend/src/Album/Edit/EditAlbumModalConnector.js deleted file mode 100644 index 7c2383f0f..000000000 --- a/frontend/src/Album/Edit/EditAlbumModalConnector.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditAlbumModal from './EditAlbumModal'; - -const mapDispatchToProps = { - clearPendingChanges -}; - -class EditAlbumModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'albums' }); - this.props.onModalClose(); - } - - // - // Render - - render() { - return ( - - ); - } -} - -EditAlbumModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(undefined, mapDispatchToProps)(EditAlbumModalConnector); diff --git a/frontend/src/Album/Edit/EditAlbumModalContent.js b/frontend/src/Album/Edit/EditAlbumModalContent.js deleted file mode 100644 index 5c70da199..000000000 --- a/frontend/src/Album/Edit/EditAlbumModalContent.js +++ /dev/null @@ -1,133 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { inputTypes } from 'Helpers/Props'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import FormInputGroup from 'Components/Form/FormInputGroup'; - -class EditAlbumModalContent extends Component { - - // - // Listeners - - onSavePress = () => { - const { - onSavePress - } = this.props; - - onSavePress(false); - - } - - // - // Render - - render() { - const { - title, - artistName, - albumType, - statistics, - item, - isSaving, - onInputChange, - onModalClose, - ...otherProps - } = this.props; - - const { - monitored, - anyReleaseOk, - releases - } = item; - - return ( - - - Edit - {artistName} - {title} [{albumType}] - - - -
- - Monitored - - - - - - Automatically Switch Release - - - - - - Release - - 0} - albumReleases={releases} - onChange={onInputChange} - /> - - -
-
- - - - - Save - - - -
- ); - } -} - -EditAlbumModalContent.propTypes = { - albumId: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - artistName: PropTypes.string.isRequired, - albumType: PropTypes.string.isRequired, - statistics: PropTypes.object.isRequired, - item: PropTypes.object.isRequired, - isSaving: PropTypes.bool.isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditAlbumModalContent; diff --git a/frontend/src/Album/Edit/EditAlbumModalContentConnector.js b/frontend/src/Album/Edit/EditAlbumModalContentConnector.js deleted file mode 100644 index f6329f8e8..000000000 --- a/frontend/src/Album/Edit/EditAlbumModalContentConnector.js +++ /dev/null @@ -1,98 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import selectSettings from 'Store/Selectors/selectSettings'; -import createAlbumSelector from 'Store/Selectors/createAlbumSelector'; -import createArtistSelector from 'Store/Selectors/createArtistSelector'; -import { setAlbumValue, saveAlbum } from 'Store/Actions/albumActions'; -import EditAlbumModalContent from './EditAlbumModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.albums, - createAlbumSelector(), - createArtistSelector(), - (albumState, album, artist) => { - const { - isSaving, - saveError, - pendingChanges - } = albumState; - - const albumSettings = _.pick(album, [ - 'monitored', - 'anyReleaseOk', - 'releases' - ]); - - const settings = selectSettings(albumSettings, pendingChanges, saveError); - - return { - title: album.title, - artistName: artist.artistName, - albumType: album.albumType, - statistics: album.statistics, - isSaving, - saveError, - item: settings.settings, - ...settings - }; - } - ); -} - -const mapDispatchToProps = { - dispatchSetAlbumValue: setAlbumValue, - dispatchSaveAlbum: saveAlbum -}; - -class EditAlbumModalContentConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.dispatchSetAlbumValue({ name, value }); - } - - onSavePress = () => { - this.props.dispatchSaveAlbum({ - id: this.props.albumId - }); - } - - // - // Render - - render() { - return ( - - ); - } -} - -EditAlbumModalContentConnector.propTypes = { - albumId: PropTypes.number, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - dispatchSetAlbumValue: PropTypes.func.isRequired, - dispatchSaveAlbum: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditAlbumModalContentConnector); diff --git a/frontend/src/Album/EpisodeStatusConnector.js b/frontend/src/Album/EpisodeStatusConnector.js index f3a390748..2c8efe5c8 100644 --- a/frontend/src/Album/EpisodeStatusConnector.js +++ b/frontend/src/Album/EpisodeStatusConnector.js @@ -46,7 +46,7 @@ class EpisodeStatusConnector extends Component { } EpisodeStatusConnector.propTypes = { - albumId: PropTypes.number.isRequired, + bookId: PropTypes.number.isRequired, trackFileId: PropTypes.number.isRequired }; diff --git a/frontend/src/Album/Search/AlbumInteractiveSearchModal.js b/frontend/src/Album/Search/AlbumInteractiveSearchModal.js index 52e825bab..d78221f29 100644 --- a/frontend/src/Album/Search/AlbumInteractiveSearchModal.js +++ b/frontend/src/Album/Search/AlbumInteractiveSearchModal.js @@ -6,7 +6,7 @@ import AlbumInteractiveSearchModalContent from './AlbumInteractiveSearchModalCon function AlbumInteractiveSearchModal(props) { const { isOpen, - albumId, + bookId, albumTitle, onModalClose } = props; @@ -18,7 +18,7 @@ function AlbumInteractiveSearchModal(props) { onModalClose={onModalClose} > @@ -28,7 +28,7 @@ function AlbumInteractiveSearchModal(props) { AlbumInteractiveSearchModal.propTypes = { isOpen: PropTypes.bool.isRequired, - albumId: PropTypes.number.isRequired, + bookId: PropTypes.number.isRequired, albumTitle: PropTypes.string.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js b/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js index ff8cbe384..2ea69efff 100644 --- a/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js +++ b/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js @@ -10,7 +10,7 @@ import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConne function AlbumInteractiveSearchModalContent(props) { const { - albumId, + bookId, albumTitle, onModalClose } = props; @@ -18,14 +18,14 @@ function AlbumInteractiveSearchModalContent(props) { return ( - Interactive Search {albumId != null && `- ${albumTitle}`} + Interactive Search {bookId != null && `- ${albumTitle}`} @@ -40,7 +40,7 @@ function AlbumInteractiveSearchModalContent(props) { } AlbumInteractiveSearchModalContent.propTypes = { - albumId: PropTypes.number.isRequired, + bookId: PropTypes.number.isRequired, albumTitle: PropTypes.string.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/AlbumStudio/AlbumStudio.js b/frontend/src/AlbumStudio/AlbumStudio.js index 145e0e1ec..a6dc8ee1e 100644 --- a/frontend/src/AlbumStudio/AlbumStudio.js +++ b/frontend/src/AlbumStudio/AlbumStudio.js @@ -41,7 +41,7 @@ const columns = [ }, { name: 'albumCount', - label: 'Albums', + label: 'Books', isSortable: false, isVisible: true } @@ -253,7 +253,7 @@ class AlbumStudio extends Component { > @@ -282,7 +282,7 @@ class AlbumStudio extends Component { onUpdateSelectedPress = (changes) => { this.props.onUpdateSelectedPress({ - artistIds: this.getSelectedIds(), + authorIds: this.getSelectedIds(), ...changes }); } diff --git a/frontend/src/AlbumStudio/AlbumStudioAlbum.js b/frontend/src/AlbumStudio/AlbumStudioAlbum.js index 8bec82840..b767c9189 100644 --- a/frontend/src/AlbumStudio/AlbumStudioAlbum.js +++ b/frontend/src/AlbumStudio/AlbumStudioAlbum.js @@ -25,7 +25,6 @@ class AlbumStudioAlbum extends Component { const { title, disambiguation, - albumType, monitored, statistics, isSaving @@ -53,14 +52,6 @@ class AlbumStudioAlbum extends Component {
-
- - { - `${albumType}` - } - -
-
- Monitor Artist + Monitor Author
- Monitor Albums + Monitor Books
- {selectedCount} Artist(s) Selected + {selectedCount} Author(s) Selected
@@ -82,9 +82,9 @@ class AlbumStudioRow extends Component { } AlbumStudioRow.propTypes = { - artistId: PropTypes.number.isRequired, + authorId: PropTypes.number.isRequired, status: PropTypes.string.isRequired, - foreignArtistId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired, monitored: PropTypes.bool.isRequired, albums: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/AlbumStudio/AlbumStudioRowConnector.js b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js index 6c3b4c45a..44cdc42ce 100644 --- a/frontend/src/AlbumStudio/AlbumStudioRowConnector.js +++ b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js @@ -13,7 +13,7 @@ const getAlbumMap = createSelector( (state) => state.albums.items, (albums) => { return albums.reduce((acc, curr) => { - (acc[curr.artistId] = acc[curr.artistId] || []).push(curr); + (acc[curr.authorId] = acc[curr.authorId] || []).push(curr); return acc; }, {}); } @@ -29,7 +29,7 @@ function createMapStateToProps() { return { ...artist, - artistId: artist.id, + authorId: artist.id, artistName: artist.artistName, monitored: artist.monitored, status: artist.status, @@ -52,20 +52,20 @@ class AlbumStudioRowConnector extends Component { onArtistMonitoredPress = () => { const { - artistId, + authorId, monitored } = this.props; this.props.toggleArtistMonitored({ - artistId, + authorId, monitored: !monitored }); } - onAlbumMonitoredPress = (albumId, monitored) => { - const albumIds = [albumId]; + onAlbumMonitoredPress = (bookId, monitored) => { + const bookIds = [bookId]; this.props.toggleAlbumsMonitored({ - albumIds, + bookIds, monitored }); } @@ -85,7 +85,7 @@ class AlbumStudioRowConnector extends Component { } AlbumStudioRowConnector.propTypes = { - artistId: PropTypes.number.isRequired, + authorId: PropTypes.number.isRequired, monitored: PropTypes.bool.isRequired, toggleArtistMonitored: PropTypes.func.isRequired, toggleAlbumsMonitored: PropTypes.func.isRequired diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index c20a591a0..81aaf096e 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -91,12 +91,12 @@ function AppRoutes(props) { /> diff --git a/frontend/src/Artist/ArtistLogo.js b/frontend/src/Artist/ArtistLogo.js deleted file mode 100644 index 05e665186..000000000 --- a/frontend/src/Artist/ArtistLogo.js +++ /dev/null @@ -1,160 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import LazyLoad from 'react-lazyload'; - -const logoPlaceholder = 'data:image/png;base64,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'; - -function findLogo(images) { - return _.find(images, { coverType: 'logo' }); -} - -function getLogoUrl(logo, size) { - if (logo) { - // Remove protocol - let url = logo.url.replace(/^https?:/, ''); - url = url.replace('logo.jpg', `logo-${size}.jpg`); - - return url; - } -} - -class ArtistLogo extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const pixelRatio = Math.floor(window.devicePixelRatio); - - const { - images, - size - } = props; - - const logo = findLogo(images); - - this.state = { - pixelRatio, - logo, - logoUrl: getLogoUrl(logo, pixelRatio * size), - hasError: false, - isLoaded: false - }; - } - - componentDidUpdate(prevProps) { - const { - images, - size - } = this.props; - - const { - pixelRatio - } = this.state; - - const logo = findLogo(images); - - if (logo && logo.url !== this.state.logo.url) { - this.setState({ - logo, - logoUrl: getLogoUrl(logo, pixelRatio * size), - hasError: false, - isLoaded: false - }); - } - } - - // - // Listeners - - onError = () => { - this.setState({ hasError: true }); - } - - onLoad = () => { - this.setState({ isLoaded: true }); - } - - // - // Render - - render() { - const { - className, - style, - size, - lazy, - overflow - } = this.props; - - const { - logoUrl, - hasError, - isLoaded - } = this.state; - - if (hasError || !logoUrl) { - return ( - - ); - } - - if (lazy) { - return ( - - } - > - - - ); - } - - return ( - - ); - } -} - -ArtistLogo.propTypes = { - className: PropTypes.string, - style: PropTypes.object, - images: PropTypes.arrayOf(PropTypes.object).isRequired, - size: PropTypes.number.isRequired, - lazy: PropTypes.bool.isRequired, - overflow: PropTypes.bool.isRequired -}; - -ArtistLogo.defaultProps = { - size: 250, - lazy: true, - overflow: false -}; - -export default ArtistLogo; diff --git a/frontend/src/Artist/ArtistNameLink.js b/frontend/src/Artist/ArtistNameLink.js index fab1cb974..930fb63f0 100644 --- a/frontend/src/Artist/ArtistNameLink.js +++ b/frontend/src/Artist/ArtistNameLink.js @@ -2,8 +2,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import Link from 'Components/Link/Link'; -function ArtistNameLink({ foreignArtistId, artistName }) { - const link = `/artist/${foreignArtistId}`; +function ArtistNameLink({ titleSlug, artistName }) { + const link = `/author/${titleSlug}`; return ( @@ -13,7 +13,7 @@ function ArtistNameLink({ foreignArtistId, artistName }) { } ArtistNameLink.propTypes = { - foreignArtistId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired }; diff --git a/frontend/src/Artist/ArtistPoster.js b/frontend/src/Artist/ArtistPoster.js index 4eebd9ca4..e830d93ba 100644 --- a/frontend/src/Artist/ArtistPoster.js +++ b/frontend/src/Artist/ArtistPoster.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import ArtistImage from './ArtistImage'; -const posterPlaceholder = 'data:image/png;base64,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'; +const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AQMAAAD7QlAQAAAABlBMVEUnJychISEIs8G4AAAEFklEQVRYw+2YMdPOQBDH95KQDEVSMKOj1FFQJx9BQc0nkeuUvoLS+AQ6MQqlRu8xCjpUgsjK7iXz2t1LMsbo8i/ey5vfc3e7m7tk9+DQoUOHDpGe3bu7hS8BwJ117BoAOLfOb/Hf62s4EY1VNrcPVvjNua1WuJ/b8xqoeR3sqFkllx8+AYAra9PniDg1ydr07cT7FQMy6k7ycQMKgJr5F4BrhvI9ZA3xCDU8fJggs9gBXJ35acX8lil74CPmO5w1xhwoIMVFMQcqKCfynH3soLLuEfkB4O5TBArDPZlH05ZkYMxBigyJDEyseylHFjjK4CzPyS4IE3gTgIxuAyulHzbG/as0PYsifM24X8/TA19Vxn2efjagNwFoHE2/GDAKpm86HE2AfMrmLQbqADnI2bzFQPv8y7NlM7naORU+uid+62X4xJg0V6PC1+KfvvSghWMgnh0cVIArCO694Ib+qWR4HQ257F9oRxu+L2FpzK3h7D5vPwqA5k1OPOwA4iaAOYWnZM4XPhPYT3eWDXriX4sHROjpskF7cC2eBHfUdVjeDw6/4Uk9oHqEz18DH9se8IvgCdQDBS/oLUxcPcB24mnAv+jfXvCMOdwI9jNXDxiJp9w9DCd4Afgdz96fF5GGk3xSCFBHw+gF4PAz9SQCwE7K5UGculJHGuTdKPun+IYHrafAUPfPKJdP4OhL7ErDuf9jfnXn6Gu6+Kj654EPKQIG7iu5PMLacGPO7Qf0EOMvx3LhhRh/5l+GOsahnPkw4Mw7sXzLedzxV+DvscsMZ8X51W0Olp/+5P7qIPlLPMEWP+3z5G94rXinuen/RWzAbe6g7hVvRX/DO8FdjMPB9+O3yD5fwf1fc72+/jcfN/cHRPZPJva/7q/27z9zlPyVfL9Abrgv/oW/Nvyx5vL9rbl5f78R/I3iTnP7fRH83QjVDpfCb4Kr71uxz1FzkN9nxfX32XKVHyj+BfweV/mJkM5Pdnkpsc6PfK64BynDM8lTiU1+l+LPP2iLUJj8sj5z3uaXgMPZFDY/rQDHs/rLTRxMfkwx4mX4hPLjaza/TgIfI/l1xvl5y/wT5+dSCd8rmXf8W2/qgx5S5rRYvAMlri+Ic2MKME9FCdQT/wJ8Ga1vSnzE+Z3l06REJi7qI1VfOXw0xusrCPVZ+6aP12dFqO/qN6d4fZeF+rB804X6sInXl/lrT1vBFtAu1KcuCfWpi9e33VLfJjZAS33ckvlZpH4uedu2nOcWhleiPr9peLFT32fyfGD7fMGBlf/jfCLZOd8oIrw6q4/o2jogzlc2z2fAW8w2nwvd3eqp0YXxCcdiS1HzRC8fw2ezJjvHVtn2tPbhqnOzTgNp1/kdv6pV7ig4RQOruuDBCax1+94dOHTo0KFDk34DoJynpPus3GIAAAAASUVORK5CYII='; function ArtistPoster(props) { return ( diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js index e0ea034ab..1632a57fa 100644 --- a/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js +++ b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js @@ -26,7 +26,7 @@ class DeleteArtistModalContentConnector extends Component { onDeletePress = (deleteFiles, addImportListExclusion) => { this.props.deleteArtist({ - id: this.props.artistId, + id: this.props.authorId, deleteFiles, addImportListExclusion }); @@ -48,7 +48,7 @@ class DeleteArtistModalContentConnector extends Component { } DeleteArtistModalContentConnector.propTypes = { - artistId: PropTypes.number.isRequired, + authorId: PropTypes.number.isRequired, onModalClose: PropTypes.func.isRequired, deleteArtist: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Details/AlbumRow.js b/frontend/src/Artist/Details/AlbumRow.js index e2d6cf65e..47f24e6c8 100644 --- a/frontend/src/Artist/Details/AlbumRow.js +++ b/frontend/src/Artist/Details/AlbumRow.js @@ -6,7 +6,6 @@ import { kinds, sizes } from 'Helpers/Props'; import TableRow from 'Components/Table/TableRow'; import Label from 'Components/Label'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; import AlbumSearchCellConnector from 'Album/AlbumSearchCellConnector'; import AlbumTitleLink from 'Album/AlbumTitleLink'; import StarRating from 'Components/StarRating'; @@ -67,19 +66,17 @@ class AlbumRow extends Component { render() { const { id, - artistId, + authorId, monitored, statistics, - duration, releaseDate, - mediumCount, - secondaryTypes, title, + position, ratings, disambiguation, isSaving, artistMonitored, - foreignAlbumId, + titleSlug, columns } = this.props; @@ -125,7 +122,7 @@ class AlbumRow extends Component { className={styles.title} > @@ -133,42 +130,13 @@ class AlbumRow extends Component { ); } - if (name === 'mediumCount') { + if (name === 'position') { return ( - - { - mediumCount - } - - ); - } - - if (name === 'secondaryTypes') { - return ( - - { - secondaryTypes - } - - ); - } - - if (name === 'trackCount') { - return ( - - { - statistics.totalTrackCount - } - - ); - } - - if (name === 'duration') { - return ( - - { - formatTimeSpan(duration) - } + + {position || ''} ); } @@ -218,8 +186,8 @@ class AlbumRow extends Component { return ( ); @@ -234,21 +202,17 @@ class AlbumRow extends Component { AlbumRow.propTypes = { id: PropTypes.number.isRequired, - artistId: PropTypes.number.isRequired, + authorId: PropTypes.number.isRequired, monitored: PropTypes.bool.isRequired, - releaseDate: PropTypes.string.isRequired, - mediumCount: PropTypes.number.isRequired, - duration: PropTypes.number.isRequired, + releaseDate: PropTypes.string, title: PropTypes.string.isRequired, + position: PropTypes.string, ratings: PropTypes.object.isRequired, disambiguation: PropTypes.string, - secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired, - foreignAlbumId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, isSaving: PropTypes.bool, - unverifiedSceneNumbering: PropTypes.bool, artistMonitored: PropTypes.bool.isRequired, statistics: PropTypes.object.isRequired, - mediaInfo: PropTypes.object, columns: PropTypes.arrayOf(PropTypes.object).isRequired, onMonitorAlbumPress: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Details/AlbumRowConnector.js b/frontend/src/Artist/Details/AlbumRowConnector.js index f00bd3fce..d93cec6a6 100644 --- a/frontend/src/Artist/Details/AlbumRowConnector.js +++ b/frontend/src/Artist/Details/AlbumRowConnector.js @@ -11,7 +11,6 @@ function createMapStateToProps() { createTrackFileSelector(), (artist = {}, trackFile) => { return { - foreignArtistId: artist.foreignArtistId, artistMonitored: artist.monitored, trackFilePath: trackFile ? trackFile.path : null }; diff --git a/frontend/src/Artist/Details/ArtistDetails.css b/frontend/src/Artist/Details/ArtistDetails.css index fb3803a85..3d572e40c 100644 --- a/frontend/src/Artist/Details/ArtistDetails.css +++ b/frontend/src/Artist/Details/ArtistDetails.css @@ -41,7 +41,6 @@ .poster { flex-shrink: 0; margin-right: 35px; - width: 250px; height: 250px; } @@ -96,6 +95,10 @@ margin-left: 20px; } +.filterIcon { + float: right; +} + .artistNavigationButtons { white-space: nowrap; } @@ -150,6 +153,31 @@ padding: 20px; } +.tabList { + margin: 0; + padding: 0; + border-bottom: 1px solid $lightGray; +} + +.tab { + position: relative; + bottom: -1px; + display: inline-block; + padding: 6px 12px; + border: 1px solid transparent; + border-top: none; + list-style: none; + cursor: pointer; +} + +.selectedTab { + border-bottom: 4px solid $linkColor; +} + +.tabContent { + margin-top: 20px; +} + @media only screen and (max-width: $breakpointSmall) { .contentContainer { padding: 20px 0; diff --git a/frontend/src/Artist/Details/ArtistDetails.js b/frontend/src/Artist/Details/ArtistDetails.js index d7cb0c7d7..d31224da5 100644 --- a/frontend/src/Artist/Details/ArtistDetails.js +++ b/frontend/src/Artist/Details/ArtistDetails.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import TextTruncate from 'react-text-truncate'; import formatBytes from 'Utilities/Number/formatBytes'; import selectAll from 'Utilities/Table/selectAll'; @@ -21,21 +22,23 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import Popover from 'Components/Tooltip/Popover'; import Tooltip from 'Components/Tooltip/Tooltip'; -import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal'; +import TrackFileEditorTable from 'TrackFile/Editor/TrackFileEditorTable'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import ArtistPoster from 'Artist/ArtistPoster'; import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; -import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal'; +import ArtistHistoryTable from 'Artist/History/ArtistHistoryTable'; import ArtistAlternateTitles from './ArtistAlternateTitles'; import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector'; +import AuthorDetailsSeriesConnector from './AuthorDetailsSeriesConnector'; import ArtistTagsConnector from './ArtistTagsConnector'; import ArtistDetailsLinks from './ArtistDetailsLinks'; import styles from './ArtistDetails.css'; +import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable'; +import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector'; import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal'; -import ArtistInteractiveSearchModalConnector from 'Artist/Search/ArtistInteractiveSearchModalConnector'; import Link from 'Components/Link/Link'; const defaultFontSize = parseInt(fonts.defaultFontSize); @@ -68,15 +71,13 @@ class ArtistDetails extends Component { this.state = { isOrganizeModalOpen: false, isRetagModalOpen: false, - isManageTracksOpen: false, isEditArtistModalOpen: false, isDeleteArtistModalOpen: false, - isArtistHistoryModalOpen: false, isInteractiveImportModalOpen: false, - isInteractiveSearchModalOpen: false, allExpanded: false, allCollapsed: false, - expandedState: {} + expandedState: {}, + selectedTabIndex: 0 }; } @@ -99,14 +100,6 @@ class ArtistDetails extends Component { this.setState({ isRetagModalOpen: false }); } - onManageTracksPress = () => { - this.setState({ isManageTracksOpen: true }); - } - - onManageTracksModalClose = () => { - this.setState({ isManageTracksOpen: false }); - } - onInteractiveImportPress = () => { this.setState({ isInteractiveImportModalOpen: true }); } @@ -115,14 +108,6 @@ class ArtistDetails extends Component { this.setState({ isInteractiveImportModalOpen: false }); } - onInteractiveSearchPress = () => { - this.setState({ isInteractiveSearchModalOpen: true }); - } - - onInteractiveSearchModalClose = () => { - this.setState({ isInteractiveSearchModalOpen: false }); - } - onEditArtistPress = () => { this.setState({ isEditArtistModalOpen: true }); } @@ -142,14 +127,6 @@ class ArtistDetails extends Component { this.setState({ isDeleteArtistModalOpen: false }); } - onArtistHistoryPress = () => { - this.setState({ isArtistHistoryModalOpen: true }); - } - - onArtistHistoryModalClose = () => { - this.setState({ isArtistHistoryModalOpen: false }); - } - onExpandAllPress = () => { const { allExpanded, @@ -159,7 +136,7 @@ class ArtistDetails extends Component { this.setState(getExpandedState(selectAll(expandedState, !allExpanded))); } - onExpandPress = (albumId, isExpanded) => { + onExpandPress = (bookId, isExpanded) => { this.setState((state) => { const convertedState = { allSelected: state.allExpanded, @@ -167,7 +144,7 @@ class ArtistDetails extends Component { selectedState: state.expandedState }; - const newState = toggleSelected(convertedState, [], albumId, isExpanded, false); + const newState = toggleSelected(convertedState, [], bookId, isExpanded, false); return getExpandedState(newState); }); @@ -179,14 +156,12 @@ class ArtistDetails extends Component { render() { const { id, - foreignArtistId, artistName, ratings, path, statistics, qualityProfileId, monitored, - albumTypes, status, overview, links, @@ -203,6 +178,8 @@ class ArtistDetails extends Component { trackFilesError, hasAlbums, hasMonitoredAlbums, + hasSeries, + series, hasTrackFiles, previousArtist, nextArtist, @@ -219,15 +196,13 @@ class ArtistDetails extends Component { const { isOrganizeModalOpen, isRetagModalOpen, - isManageTracksOpen, isEditArtistModalOpen, isDeleteArtistModalOpen, - isArtistHistoryModalOpen, isInteractiveImportModalOpen, - isInteractiveSearchModalOpen, allExpanded, allCollapsed, - expandedState + expandedState, + selectedTabIndex } = this.state; const continuing = status === 'continuing'; @@ -271,15 +246,6 @@ class ArtistDetails extends Component { onPress={onSearchPress} /> - - - - - - - + {/* */}
@@ -528,7 +480,6 @@ class ArtistDetails extends Component { } tooltip={ } @@ -554,7 +505,7 @@ class ArtistDetails extends Component { } - tooltip={} + tooltip={} kind={kinds.INVERSE} position={tooltipPositions.BOTTOM} /> @@ -564,7 +515,7 @@ class ArtistDetails extends Component {
]*>?/gm, '')} />
@@ -588,30 +539,110 @@ class ArtistDetails extends Component { } { - isPopulated && !!albumTypes.length && -
- { - albumTypes.slice(0).map((albumType) => { - return ( - - ); - }) - } -
+ isPopulated && + this.setState({ selectedTabIndex: tabIndex })}> + + + Books + + + + Series + + + + History + + + + Search + + + + Files + + + { + selectedTabIndex === 3 && +
+ +
+ } +
+ + + + + + + { + isPopulated && hasSeries && +
+ { + series.map((item) => { + return ( + + ); + }) + } +
+ } +
+ + + + + + + + + + + + +
}
- Missing Albums, Singles, or Other Types? Modify or create a new + Missing or too many books? Modify or create a new Metadata Profile or manually Search @@ -620,38 +651,26 @@ class ArtistDetails extends Component { - - - - @@ -663,12 +682,6 @@ class ArtistDetails extends Component { showImportMode={false} onModalClose={this.onInteractiveImportModalClose} /> - - ); @@ -677,7 +690,6 @@ class ArtistDetails extends Component { ArtistDetails.propTypes = { id: PropTypes.number.isRequired, - foreignArtistId: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired, ratings: PropTypes.object.isRequired, path: PropTypes.string.isRequired, @@ -685,7 +697,6 @@ ArtistDetails.propTypes = { qualityProfileId: PropTypes.number.isRequired, monitored: PropTypes.bool.isRequired, artistType: PropTypes.string, - albumTypes: PropTypes.arrayOf(PropTypes.string), status: PropTypes.string.isRequired, overview: PropTypes.string.isRequired, links: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -701,6 +712,8 @@ ArtistDetails.propTypes = { trackFilesError: PropTypes.object, hasAlbums: PropTypes.bool.isRequired, hasMonitoredAlbums: PropTypes.bool.isRequired, + hasSeries: PropTypes.bool.isRequired, + series: PropTypes.arrayOf(PropTypes.object).isRequired, hasTrackFiles: PropTypes.bool.isRequired, previousArtist: PropTypes.object.isRequired, nextArtist: PropTypes.object.isRequired, diff --git a/frontend/src/Artist/Details/ArtistDetailsConnector.js b/frontend/src/Artist/Details/ArtistDetailsConnector.js index 28fa0381d..4b0356e14 100644 --- a/frontend/src/Artist/Details/ArtistDetailsConnector.js +++ b/frontend/src/Artist/Details/ArtistDetailsConnector.js @@ -6,12 +6,15 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { findCommand, isCommandExecuting } from 'Utilities/Command'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions'; +import { fetchSeries, clearSeries } from 'Store/Actions/seriesActions'; import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions'; import { toggleArtistMonitored } from 'Store/Actions/artistActions'; import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; +import { clearReleases, cancelFetchReleases } from 'Store/Actions/releaseActions'; import { executeCommand } from 'Store/Actions/commandActions'; import * as commandNames from 'Commands/commandNames'; import ArtistDetails from './ArtistDetails'; @@ -28,15 +31,36 @@ const selectAlbums = createSelector( const hasAlbums = !!items.length; const hasMonitoredAlbums = items.some((e) => e.monitored); - const albumTypes = _.uniq(_.map(items, 'albumType')); return { isAlbumsFetching: isFetching, isAlbumsPopulated: isPopulated, albumsError: error, hasAlbums, - hasMonitoredAlbums, - albumTypes + hasMonitoredAlbums + }; + } +); + +const selectSeries = createSelector( + createSortedSectionSelector('series', (a, b) => a.title.localeCompare(b.title)), + (state) => state.series, + (series) => { + const { + items, + isFetching, + isPopulated, + error + } = series; + + const hasSeries = !!items.length; + + return { + isSeriesFetching: isFetching, + isSeriesPopulated: isPopulated, + seriesError: error, + hasSeries, + series: series.items }; } ); @@ -64,14 +88,15 @@ const selectTrackFiles = createSelector( function createMapStateToProps() { return createSelector( - (state, { foreignArtistId }) => foreignArtistId, + (state, { titleSlug }) => titleSlug, selectAlbums, + selectSeries, selectTrackFiles, createAllArtistSelector(), createCommandsSelector(), - (foreignArtistId, albums, trackFiles, allArtists, commands) => { + (titleSlug, albums, series, trackFiles, allArtists, commands) => { const sortedArtist = _.orderBy(allArtists, 'sortName'); - const artistIndex = _.findIndex(sortedArtist, { foreignArtistId }); + const artistIndex = _.findIndex(sortedArtist, { titleSlug }); const artist = sortedArtist[artistIndex]; if (!artist) { @@ -83,10 +108,17 @@ function createMapStateToProps() { isAlbumsPopulated, albumsError, hasAlbums, - hasMonitoredAlbums, - albumTypes + hasMonitoredAlbums } = albums; + const { + isSeriesFetching, + isSeriesPopulated, + seriesError, + hasSeries, + series: seriesItems + } = series; + const { isTrackFilesFetching, isTrackFilesPopulated, @@ -94,28 +126,26 @@ function createMapStateToProps() { hasTrackFiles } = trackFiles; - const sortedAlbumTypes = _.orderBy(albumTypes); - const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist); const nextArtist = sortedArtist[artistIndex + 1] || _.first(sortedArtist); - const isArtistRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: artist.id })); + const isArtistRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_ARTIST, authorId: artist.id })); const artistRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_ARTIST }); const allArtistRefreshing = ( isCommandExecuting(artistRefreshingCommand) && - !artistRefreshingCommand.body.artistId + !artistRefreshingCommand.body.authorId ); const isRefreshing = isArtistRefreshing || allArtistRefreshing; - const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id })); - const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id })); + const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, authorId: artist.id })); + const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: artist.id })); const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST }); const isRenamingArtist = ( isCommandExecuting(isRenamingArtistCommand) && - isRenamingArtistCommand.body.artistIds.indexOf(artist.id) > -1 + isRenamingArtistCommand.body.authorIds.indexOf(artist.id) > -1 ); - const isFetching = isAlbumsFetching || isTrackFilesFetching; - const isPopulated = isAlbumsPopulated && isTrackFilesPopulated; + const isFetching = isAlbumsFetching || isSeriesFetching || isTrackFilesFetching; + const isPopulated = isAlbumsPopulated && isSeriesPopulated && isTrackFilesPopulated; const alternateTitles = _.reduce(artist.alternateTitles, (acc, alternateTitle) => { if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) && @@ -128,7 +158,6 @@ function createMapStateToProps() { return { ...artist, - albumTypes: sortedAlbumTypes, alternateTitles, isArtistRefreshing, allArtistRefreshing, @@ -139,9 +168,12 @@ function createMapStateToProps() { isFetching, isPopulated, albumsError, + seriesError, trackFilesError, hasAlbums, hasMonitoredAlbums, + hasSeries, + series: seriesItems, hasTrackFiles, previousArtist, nextArtist @@ -153,11 +185,15 @@ function createMapStateToProps() { const mapDispatchToProps = { fetchAlbums, clearAlbums, + fetchSeries, + clearSeries, fetchTrackFiles, clearTrackFiles, toggleArtistMonitored, fetchQueueDetails, clearQueueDetails, + clearReleases, + cancelFetchReleases, executeCommand }; @@ -207,17 +243,21 @@ class ArtistDetailsConnector extends Component { // Control populate = () => { - const artistId = this.props.id; + const authorId = this.props.id; - this.props.fetchAlbums({ artistId }); - this.props.fetchTrackFiles({ artistId }); - this.props.fetchQueueDetails({ artistId }); + this.props.fetchAlbums({ authorId }); + this.props.fetchSeries({ authorId }); + this.props.fetchTrackFiles({ authorId }); + this.props.fetchQueueDetails({ authorId }); } unpopulate = () => { + this.props.cancelFetchReleases(); this.props.clearAlbums(); + this.props.clearSeries(); this.props.clearTrackFiles(); this.props.clearQueueDetails(); + this.props.clearReleases(); } // @@ -225,7 +265,7 @@ class ArtistDetailsConnector extends Component { onMonitorTogglePress = (monitored) => { this.props.toggleArtistMonitored({ - artistId: this.props.id, + authorId: this.props.id, monitored }); } @@ -233,14 +273,14 @@ class ArtistDetailsConnector extends Component { onRefreshPress = () => { this.props.executeCommand({ name: commandNames.REFRESH_ARTIST, - artistId: this.props.id + authorId: this.props.id }); } onSearchPress = () => { this.props.executeCommand({ name: commandNames.ARTIST_SEARCH, - artistId: this.props.id + authorId: this.props.id }); } @@ -261,7 +301,7 @@ class ArtistDetailsConnector extends Component { ArtistDetailsConnector.propTypes = { id: PropTypes.number.isRequired, - foreignArtistId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, isArtistRefreshing: PropTypes.bool.isRequired, allArtistRefreshing: PropTypes.bool.isRequired, isRefreshing: PropTypes.bool.isRequired, @@ -269,11 +309,15 @@ ArtistDetailsConnector.propTypes = { isRenamingArtist: PropTypes.bool.isRequired, fetchAlbums: PropTypes.func.isRequired, clearAlbums: PropTypes.func.isRequired, + fetchSeries: PropTypes.func.isRequired, + clearSeries: PropTypes.func.isRequired, fetchTrackFiles: PropTypes.func.isRequired, clearTrackFiles: PropTypes.func.isRequired, toggleArtistMonitored: PropTypes.func.isRequired, fetchQueueDetails: PropTypes.func.isRequired, clearQueueDetails: PropTypes.func.isRequired, + clearReleases: PropTypes.func.isRequired, + cancelFetchReleases: PropTypes.func.isRequired, executeCommand: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Details/ArtistDetailsLinks.js b/frontend/src/Artist/Details/ArtistDetailsLinks.js index 23941d06b..47d7d3de6 100644 --- a/frontend/src/Artist/Details/ArtistDetailsLinks.js +++ b/frontend/src/Artist/Details/ArtistDetailsLinks.js @@ -7,26 +7,12 @@ import styles from './ArtistDetailsLinks.css'; function ArtistDetailsLinks(props) { const { - foreignArtistId, links } = props; return (
- - - - {links.map((link, index) => { return ( @@ -56,7 +42,6 @@ function ArtistDetailsLinks(props) { } ArtistDetailsLinks.propTypes = { - foreignArtistId: PropTypes.string.isRequired, links: PropTypes.arrayOf(PropTypes.object).isRequired }; diff --git a/frontend/src/Artist/Details/ArtistDetailsPageConnector.js b/frontend/src/Artist/Details/ArtistDetailsPageConnector.js index cdf722161..aac5855a1 100644 --- a/frontend/src/Artist/Details/ArtistDetailsPageConnector.js +++ b/frontend/src/Artist/Details/ArtistDetailsPageConnector.js @@ -17,7 +17,7 @@ function createMapStateToProps() { (state, { match }) => match, (state) => state.artist, (match, artist) => { - const foreignArtistId = match.params.foreignArtistId; + const titleSlug = match.params.titleSlug; const { isFetching, isPopulated, @@ -25,13 +25,13 @@ function createMapStateToProps() { items } = artist; - const artistIndex = _.findIndex(items, { foreignArtistId }); + const artistIndex = _.findIndex(items, { titleSlug }); if (artistIndex > -1) { return { isFetching, isPopulated, - foreignArtistId + titleSlug }; } @@ -54,7 +54,7 @@ class ArtistDetailsPageConnector extends Component { // Lifecycle componentDidUpdate(prevProps) { - if (!this.props.foreignArtistId) { + if (!this.props.titleSlug) { this.props.push(`${window.Readarr.urlBase}/`); return; } @@ -65,7 +65,7 @@ class ArtistDetailsPageConnector extends Component { render() { const { - foreignArtistId, + titleSlug, isFetching, isPopulated, error @@ -84,33 +84,33 @@ class ArtistDetailsPageConnector extends Component { if (!isFetching && !!error) { return (
- {getErrorMessage(error, 'Failed to load artist from API')} + {getErrorMessage(error, 'Failed to load author from API')}
); } - if (!foreignArtistId) { + if (!titleSlug) { return ( ); } return ( ); } } ArtistDetailsPageConnector.propTypes = { - foreignArtistId: PropTypes.string, + titleSlug: PropTypes.string, isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, - match: PropTypes.shape({ params: PropTypes.shape({ foreignArtistId: PropTypes.string.isRequired }).isRequired }).isRequired, + match: PropTypes.shape({ params: PropTypes.shape({ titleSlug: PropTypes.string.isRequired }).isRequired }).isRequired, push: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Details/ArtistDetailsSeason.js b/frontend/src/Artist/Details/ArtistDetailsSeason.js index f9968a8e9..6fdc8f218 100644 --- a/frontend/src/Artist/Details/ArtistDetailsSeason.js +++ b/frontend/src/Artist/Details/ArtistDetailsSeason.js @@ -2,14 +2,9 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import getToggledRange from 'Utilities/Table/getToggledRange'; -import { icons, sortDirections } from 'Helpers/Props'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; +import { sortDirections } from 'Helpers/Props'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal'; -import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import AlbumRowConnector from './AlbumRowConnector'; import styles from './ArtistDetailsSeason.css'; @@ -22,92 +17,29 @@ class ArtistDetailsSeason extends Component { super(props, context); this.state = { - isOrganizeModalOpen: false, - isManageTracksOpen: false, lastToggledAlbum: null }; } - componentDidMount() { - this._expandByDefault(); - } - - componentDidUpdate(prevProps) { - const { - artistId - } = this.props; - - if (prevProps.artistId !== artistId) { - this._expandByDefault(); - return; - } - } - - // - // Control - - _expandByDefault() { - const { - name, - onExpandPress, - items, - uiSettings - } = this.props; - - const expand = _.some(items, (item) => - ((item.albumType === 'Album') && uiSettings.expandAlbumByDefault) || - ((item.albumType === 'Single') && uiSettings.expandSingleByDefault) || - ((item.albumType === 'EP') && uiSettings.expandEPByDefault) || - ((item.albumType === 'Broadcast') && uiSettings.expandBroadcastByDefault) || - ((item.albumType === 'Other') && uiSettings.expandOtherByDefault)); - - onExpandPress(name, expand); - } - // // Listeners - onOrganizePress = () => { - this.setState({ isOrganizeModalOpen: true }); - } - - onOrganizeModalClose = () => { - this.setState({ isOrganizeModalOpen: false }); - } - - onManageTracksPress = () => { - this.setState({ isManageTracksOpen: true }); - } - - onManageTracksModalClose = () => { - this.setState({ isManageTracksOpen: false }); - } - - onExpandPress = () => { - const { - name, - isExpanded - } = this.props; - - this.props.onExpandPress(name, !isExpanded); - } - - onMonitorAlbumPress = (albumId, monitored, { shiftKey }) => { + onMonitorAlbumPress = (bookId, monitored, { shiftKey }) => { const lastToggled = this.state.lastToggledAlbum; - const albumIds = [albumId]; + const bookIds = [bookId]; if (shiftKey && lastToggled) { - const { lower, upper } = getToggledRange(this.props.items, albumId, lastToggled); + const { lower, upper } = getToggledRange(this.props.items, bookId, lastToggled); const items = this.props.items; for (let i = lower; i < upper; i++) { - albumIds.push(items[i].id); + bookIds.push(items[i].id); } } - this.setState({ lastToggledAlbum: albumId }); + this.setState({ lastToggledAlbum: bookId }); - this.props.onMonitorAlbumPress(_.uniq(albumIds), monitored); + this.props.onMonitorAlbumPress(_.uniq(bookIds), monitored); } // @@ -115,134 +47,52 @@ class ArtistDetailsSeason extends Component { render() { const { - artistId, - label, items, columns, - isExpanded, sortKey, sortDirection, onSortPress, - isSmallScreen, onTableOptionChange } = this.props; - const { - isOrganizeModalOpen, - isManageTracksOpen - } = this.state; - return (
- -
-
+
+ + { -
- - {label} - - - - ({items.length} Releases) - -
- } - - - - - - { - !isSmallScreen && -   - } - - - - -
- { - isExpanded && -
- { - items.length ? -
{ + return ( + - - { - items.map((item) => { - return ( - - ); - }) - } - -
: - -
- No releases in this group -
- } -
- -
-
- } + {...item} + onMonitorAlbumPress={this.onMonitorAlbumPress} + /> + ); + }) + } + +
- - - -
); } } ArtistDetailsSeason.propTypes = { - artistId: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, sortKey: PropTypes.string, sortDirection: PropTypes.oneOf(sortDirections.all), items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, - isExpanded: PropTypes.bool, - isSmallScreen: PropTypes.bool.isRequired, onTableOptionChange: PropTypes.func.isRequired, onExpandPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired, diff --git a/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js index ffb84ba2c..48f81fd51 100644 --- a/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js +++ b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js @@ -23,7 +23,7 @@ function createMapStateToProps() { createUISettingsSelector(), (label, albums, artist, commands, dimensions, uiSettings) => { - const albumsInGroup = _.filter(albums.items, { albumType: label }); + const albumsInGroup = albums.items; let sortDir = 'asc'; @@ -66,9 +66,9 @@ class ArtistDetailsSeasonConnector extends Component { this.props.dispatchSetAlbumSort({ sortKey }); } - onMonitorAlbumPress = (albumIds, monitored) => { + onMonitorAlbumPress = (bookIds, monitored) => { this.props.toggleAlbumsMonitored({ - albumIds, + bookIds, monitored }); } @@ -89,7 +89,7 @@ class ArtistDetailsSeasonConnector extends Component { } ArtistDetailsSeasonConnector.propTypes = { - artistId: PropTypes.number.isRequired, + authorId: PropTypes.number.isRequired, toggleAlbumsMonitored: PropTypes.func.isRequired, setAlbumsTableOption: PropTypes.func.isRequired, dispatchSetAlbumSort: PropTypes.func.isRequired, diff --git a/frontend/src/Album/Details/AlbumDetailsMedium.css b/frontend/src/Artist/Details/AuthorDetailsSeries.css similarity index 74% rename from frontend/src/Album/Details/AlbumDetailsMedium.css rename to frontend/src/Artist/Details/AuthorDetailsSeries.css index 67418316d..21591d93e 100644 --- a/frontend/src/Album/Details/AlbumDetailsMedium.css +++ b/frontend/src/Artist/Details/AuthorDetailsSeries.css @@ -1,4 +1,4 @@ -.medium { +.albumType { margin-bottom: 20px; border: 1px solid $borderColor; border-radius: 4px; @@ -15,31 +15,36 @@ align-items: center; width: 100%; font-size: 24px; + cursor: pointer; } -.mediumNumber { - margin-right: 10px; +.albumTypeLabel { + margin-right: 5px; margin-left: 5px; } -.mediumFormat { +.albumCount { color: #8895aa; font-style: italic; font-size: 18px; } +.episodeCountTooltip { + display: flex; +} + .expandButton { composes: link from '~Components/Link/Link.css'; flex-grow: 1; - margin: 0 20px; + width: 100%; text-align: center; } .left { display: flex; align-items: center; - flex: 0 1 300px; + flex: 1 1 300px; } .left, @@ -57,7 +62,7 @@ composes: menuContent from '~Components/Menu/MenuContent.css'; white-space: nowrap; - font-size: 14px; + font-size: $defaultFontSize; } .actionMenuIcon { @@ -70,38 +75,46 @@ width: 30px; } -.tracks { +.albums { padding-top: 15px; border-top: 1px solid $borderColor; } .collapseButtonContainer { + display: flex; + align-items: center; + justify-content: center; padding: 10px 15px; width: 100%; border-top: 1px solid $borderColor; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: #fafafa; - text-align: center; +} + +.collapseButtonIcon { + margin-bottom: -4px; } .expandButtonIcon { composes: actionButton; - position: absolute; - top: 50%; - left: 50%; - margin-top: -12px; - margin-left: -15px; + margin-right: 15px; + + /* position: absolute; */ + /* top: 50%; */ + /* left: 90%; */ + /* margin-top: -12px; */ + /* margin-left: -15px; */ } -.noTracks { +.noAlbums { margin-bottom: 15px; text-align: center; } @media only screen and (max-width: $breakpointSmall) { - .medium { + .albumType { border-right: 0; border-left: 0; border-radius: 0; diff --git a/frontend/src/Artist/Details/AuthorDetailsSeries.js b/frontend/src/Artist/Details/AuthorDetailsSeries.js new file mode 100644 index 000000000..d28b90635 --- /dev/null +++ b/frontend/src/Artist/Details/AuthorDetailsSeries.js @@ -0,0 +1,205 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getToggledRange from 'Utilities/Table/getToggledRange'; +import { icons, sortDirections } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import AlbumRowConnector from './AlbumRowConnector'; +import styles from './AuthorDetailsSeries.css'; + +class AuthorDetailsSeries extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isOrganizeModalOpen: false, + isManageTracksOpen: false, + lastToggledAlbum: null + }; + } + + componentDidMount() { + this._expandByDefault(); + } + + componentDidUpdate(prevProps) { + const { + authorId + } = this.props; + + if (prevProps.authorId !== authorId) { + this._expandByDefault(); + return; + } + } + + // + // Control + + _expandByDefault() { + const { + id, + onExpandPress + } = this.props; + + onExpandPress(id, true); + } + + // + // Listeners + + onExpandPress = () => { + const { + id, + isExpanded + } = this.props; + + this.props.onExpandPress(id, !isExpanded); + } + + onMonitorAlbumPress = (albumId, monitored, { shiftKey }) => { + const lastToggled = this.state.lastToggledAlbum; + const albumIds = [albumId]; + + if (shiftKey && lastToggled) { + const { lower, upper } = getToggledRange(this.props.items, albumId, lastToggled); + const items = this.props.items; + + for (let i = lower; i < upper; i++) { + albumIds.push(items[i].id); + } + } + + this.setState({ lastToggledAlbum: albumId }); + + this.props.onMonitorAlbumPress(_.uniq(albumIds), monitored); + } + + // + // Render + + render() { + const { + label, + items, + positionMap, + columns, + isExpanded, + sortKey, + sortDirection, + onSortPress, + isSmallScreen, + onTableOptionChange + } = this.props; + + return ( +
+ +
+
+ { +
+ + {label} + + + + ({items.length} Books) + +
+ } + +
+ + + + { + !isSmallScreen && +   + } + +
+ + +
+ { + isExpanded && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ +
+ +
+
+ } +
+
+ ); + } +} + +AuthorDetailsSeries.propTypes = { + id: PropTypes.number.isRequired, + authorId: PropTypes.number.isRequired, + label: PropTypes.string.isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + items: PropTypes.arrayOf(PropTypes.object).isRequired, + positionMap: PropTypes.object.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + isExpanded: PropTypes.bool, + isSmallScreen: PropTypes.bool.isRequired, + onTableOptionChange: PropTypes.func.isRequired, + onExpandPress: PropTypes.func.isRequired, + onSortPress: PropTypes.func.isRequired, + onMonitorAlbumPress: PropTypes.func.isRequired, + uiSettings: PropTypes.object.isRequired +}; + +export default AuthorDetailsSeries; diff --git a/frontend/src/Artist/Details/AuthorDetailsSeriesConnector.js b/frontend/src/Artist/Details/AuthorDetailsSeriesConnector.js new file mode 100644 index 000000000..8f38324f7 --- /dev/null +++ b/frontend/src/Artist/Details/AuthorDetailsSeriesConnector.js @@ -0,0 +1,121 @@ +/* eslint max-params: 0 */ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +// import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { toggleAlbumsMonitored, setAlbumsTableOption } from 'Store/Actions/albumActions'; +import { setSeriesSort } from 'Store/Actions/seriesActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import AuthorDetailsSeries from './AuthorDetailsSeries'; + +function createMapStateToProps() { + return createSelector( + (state, { seriesId }) => seriesId, + (state) => state.albums, + createArtistSelector(), + (state) => state.series, + createCommandsSelector(), + createDimensionsSelector(), + createUISettingsSelector(), + (seriesId, books, author, series, commands, dimensions, uiSettings) => { + + const currentSeries = _.find(series.items, { id: seriesId }); + + const bookIds = currentSeries.links.map((x) => x.bookId); + const positionMap = currentSeries.links.reduce((acc, curr) => { + acc[curr.bookId] = curr.position; + return acc; + }, {}); + + const booksInSeries = _.filter(books.items, (book) => bookIds.includes(book.id)); + + let sortDir = 'asc'; + + if (series.sortDirection === 'descending') { + sortDir = 'desc'; + } + + let sortedBooks = []; + if (series.sortKey === 'position') { + sortedBooks = booksInSeries.sort((a, b) => { + const apos = positionMap[a.id] || ''; + const bpos = positionMap[b.id] || ''; + return apos.localeCompare(bpos, undefined, { numeric: true, sensivity: 'base' }); + }); + } else { + sortedBooks = _.orderBy(booksInSeries, series.sortKey, sortDir); + } + + return { + id: currentSeries.id, + label: currentSeries.title, + items: sortedBooks, + positionMap, + columns: series.columns, + sortKey: series.sortKey, + sortDirection: series.sortDirection, + artistMonitored: author.monitored, + isSmallScreen: dimensions.isSmallScreen, + uiSettings + }; + } + ); +} + +const mapDispatchToProps = { + toggleAlbumsMonitored, + setAlbumsTableOption, + dispatchSetSeriesSort: setSeriesSort, + executeCommand +}; + +class ArtistDetailsSeasonConnector extends Component { + + // + // Listeners + + onTableOptionChange = (payload) => { + this.props.setAlbumsTableOption(payload); + } + + onSortPress = (sortKey) => { + this.props.dispatchSetSeriesSort({ sortKey }); + } + + onMonitorAlbumPress = (bookIds, monitored) => { + this.props.toggleAlbumsMonitored({ + bookIds, + monitored + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ArtistDetailsSeasonConnector.propTypes = { + authorId: PropTypes.number.isRequired, + toggleAlbumsMonitored: PropTypes.func.isRequired, + setAlbumsTableOption: PropTypes.func.isRequired, + dispatchSetSeriesSort: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsSeasonConnector); diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.js b/frontend/src/Artist/Edit/EditArtistModalContent.js index adeb61c1e..c1e5a40c7 100644 --- a/frontend/src/Artist/Edit/EditArtistModalContent.js +++ b/frontend/src/Artist/Edit/EditArtistModalContent.js @@ -72,7 +72,6 @@ class EditArtistModalContent extends Component { const { monitored, - albumFolder, qualityProfileId, metadataProfileId, path, @@ -99,18 +98,6 @@ class EditArtistModalContent extends Component { /> - - Use Album Folder - - - - Quality Profile @@ -213,7 +200,7 @@ class EditArtistModalContent extends Component { } EditArtistModalContent.propTypes = { - artistId: PropTypes.number.isRequired, + authorId: PropTypes.number.isRequired, artistName: PropTypes.string.isRequired, item: PropTypes.object.isRequired, isSaving: PropTypes.bool.isRequired, diff --git a/frontend/src/Artist/Edit/EditArtistModalContentConnector.js b/frontend/src/Artist/Edit/EditArtistModalContentConnector.js index 351bc7d34..98e85d689 100644 --- a/frontend/src/Artist/Edit/EditArtistModalContentConnector.js +++ b/frontend/src/Artist/Edit/EditArtistModalContentConnector.js @@ -87,7 +87,7 @@ class EditArtistModalContentConnector extends Component { onSavePress = (moveFiles) => { this.props.dispatchSaveArtist({ - id: this.props.artistId, + id: this.props.authorId, moveFiles }); } @@ -108,7 +108,7 @@ class EditArtistModalContentConnector extends Component { } EditArtistModalContentConnector.propTypes = { - artistId: PropTypes.number, + authorId: PropTypes.number, isSaving: PropTypes.bool.isRequired, saveError: PropTypes.object, dispatchSetArtistValue: PropTypes.func.isRequired, diff --git a/frontend/src/Artist/Editor/ArtistEditor.js b/frontend/src/Artist/Editor/ArtistEditor.js index d4f6b282c..8eba0ce90 100644 --- a/frontend/src/Artist/Editor/ArtistEditor.js +++ b/frontend/src/Artist/Editor/ArtistEditor.js @@ -45,12 +45,6 @@ function getColumns(showMetadataProfile) { isSortable: true, isVisible: showMetadataProfile }, - { - name: 'albumFolder', - label: 'Album Folder', - isSortable: true, - isVisible: true - }, { name: 'path', label: 'Path', @@ -122,7 +116,7 @@ class ArtistEditor extends Component { onSaveSelected = (changes) => { this.props.onSaveSelected({ - artistIds: this.getSelectedIds(), + authorIds: this.getSelectedIds(), ...changes }); } @@ -184,7 +178,7 @@ class ArtistEditor extends Component { columns } = this.state; - const selectedArtistIds = this.getSelectedIds(); + const selectedAuthorIds = this.getSelectedIds(); return ( @@ -252,8 +246,8 @@ class ArtistEditor extends Component { diff --git a/frontend/src/Artist/Editor/ArtistEditorFooter.js b/frontend/src/Artist/Editor/ArtistEditorFooter.js index 0a94e6bbb..39556710e 100644 --- a/frontend/src/Artist/Editor/ArtistEditorFooter.js +++ b/frontend/src/Artist/Editor/ArtistEditorFooter.js @@ -137,7 +137,7 @@ class ArtistEditorFooter extends Component { render() { const { - artistIds, + authorIds, selectedCount, isSaving, isDeleting, @@ -309,14 +309,14 @@ class ArtistEditorFooter extends Component { @@ -333,7 +333,7 @@ class ArtistEditorFooter extends Component { } ArtistEditorFooter.propTypes = { - artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + authorIds: PropTypes.arrayOf(PropTypes.number).isRequired, selectedCount: PropTypes.number.isRequired, isSaving: PropTypes.bool.isRequired, saveError: PropTypes.object, diff --git a/frontend/src/Artist/Editor/ArtistEditorRow.js b/frontend/src/Artist/Editor/ArtistEditorRow.js index cfead73be..ddf35c228 100644 --- a/frontend/src/Artist/Editor/ArtistEditorRow.js +++ b/frontend/src/Artist/Editor/ArtistEditorRow.js @@ -2,7 +2,6 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import TagListConnector from 'Components/TagListConnector'; -import CheckInput from 'Components/Form/CheckInput'; import TableRow from 'Components/Table/TableRow'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; @@ -27,13 +26,12 @@ class ArtistEditorRow extends Component { const { id, status, - foreignArtistId, + titleSlug, artistName, artistType, monitored, metadataProfile, qualityProfile, - albumFolder, path, tags, columns, @@ -57,7 +55,7 @@ class ArtistEditorRow extends Component { @@ -73,15 +71,6 @@ class ArtistEditorRow extends Component { } - - - - {path} @@ -99,13 +88,12 @@ class ArtistEditorRow extends Component { ArtistEditorRow.propTypes = { id: PropTypes.number.isRequired, status: PropTypes.string.isRequired, - foreignArtistId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired, artistType: PropTypes.string, monitored: PropTypes.bool.isRequired, metadataProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired, - albumFolder: PropTypes.bool.isRequired, path: PropTypes.string.isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Artist/Editor/AudioTags/RetagArtistModalContentConnector.js b/frontend/src/Artist/Editor/AudioTags/RetagArtistModalContentConnector.js index 1c104db00..23777b535 100644 --- a/frontend/src/Artist/Editor/AudioTags/RetagArtistModalContentConnector.js +++ b/frontend/src/Artist/Editor/AudioTags/RetagArtistModalContentConnector.js @@ -10,10 +10,10 @@ import RetagArtistModalContent from './RetagArtistModalContent'; function createMapStateToProps() { return createSelector( - (state, { artistIds }) => artistIds, + (state, { authorIds }) => authorIds, createAllArtistSelector(), - (artistIds, allArtists) => { - const artist = _.intersectionWith(allArtists, artistIds, (s, id) => { + (authorIds, allArtists) => { + const artist = _.intersectionWith(allArtists, authorIds, (s, id) => { return s.id === id; }); @@ -39,7 +39,7 @@ class RetagArtistModalContentConnector extends Component { onRetagArtistPress = () => { this.props.executeCommand({ name: commandNames.RETAG_ARTIST, - artistIds: this.props.artistIds + authorIds: this.props.authorIds }); this.props.onModalClose(true); @@ -59,7 +59,7 @@ class RetagArtistModalContentConnector extends Component { } RetagArtistModalContentConnector.propTypes = { - artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + authorIds: PropTypes.arrayOf(PropTypes.number).isRequired, onModalClose: PropTypes.func.isRequired, executeCommand: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js index 8c61976e8..6479f0bd7 100644 --- a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js @@ -7,10 +7,10 @@ import DeleteArtistModalContent from './DeleteArtistModalContent'; function createMapStateToProps() { return createSelector( - (state, { artistIds }) => artistIds, + (state, { authorIds }) => authorIds, createAllArtistSelector(), - (artistIds, allArtists) => { - const selectedArtist = _.intersectionWith(allArtists, artistIds, (s, id) => { + (authorIds, allArtists) => { + const selectedArtist = _.intersectionWith(allArtists, authorIds, (s, id) => { return s.id === id; }); @@ -33,7 +33,7 @@ function createMapDispatchToProps(dispatch, props) { return { onDeleteSelectedPress(deleteFiles) { dispatch(bulkDeleteArtist({ - artistIds: props.artistIds, + authorIds: props.authorIds, deleteFiles })); diff --git a/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js index 6be1eb961..64fe4c09c 100644 --- a/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js +++ b/frontend/src/Artist/Editor/Organize/OrganizeArtistModalContentConnector.js @@ -10,10 +10,10 @@ import OrganizeArtistModalContent from './OrganizeArtistModalContent'; function createMapStateToProps() { return createSelector( - (state, { artistIds }) => artistIds, + (state, { authorIds }) => authorIds, createAllArtistSelector(), - (artistIds, allArtists) => { - const artist = _.intersectionWith(allArtists, artistIds, (s, id) => { + (authorIds, allArtists) => { + const artist = _.intersectionWith(allArtists, authorIds, (s, id) => { return s.id === id; }); @@ -39,7 +39,7 @@ class OrganizeArtistModalContentConnector extends Component { onOrganizeArtistPress = () => { this.props.executeCommand({ name: commandNames.RENAME_ARTIST, - artistIds: this.props.artistIds + authorIds: this.props.authorIds }); this.props.onModalClose(true); @@ -59,7 +59,7 @@ class OrganizeArtistModalContentConnector extends Component { } OrganizeArtistModalContentConnector.propTypes = { - artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + authorIds: PropTypes.arrayOf(PropTypes.number).isRequired, onModalClose: PropTypes.func.isRequired, executeCommand: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js b/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js index 6741e8b5c..85b076706 100644 --- a/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js +++ b/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js @@ -7,11 +7,11 @@ import TagsModalContent from './TagsModalContent'; function createMapStateToProps() { return createSelector( - (state, { artistIds }) => artistIds, + (state, { authorIds }) => authorIds, createAllArtistSelector(), createTagsSelector(), - (artistIds, allArtists, tagList) => { - const artist = _.intersectionWith(allArtists, artistIds, (s, id) => { + (authorIds, allArtists, tagList) => { + const artist = _.intersectionWith(allArtists, authorIds, (s, id) => { return s.id === id; }); diff --git a/frontend/src/Artist/History/ArtistHistoryModalContentConnector.js b/frontend/src/Artist/History/ArtistHistoryContentConnector.js similarity index 71% rename from frontend/src/Artist/History/ArtistHistoryModalContentConnector.js rename to frontend/src/Artist/History/ArtistHistoryContentConnector.js index a989361f5..ab5b38ba5 100644 --- a/frontend/src/Artist/History/ArtistHistoryModalContentConnector.js +++ b/frontend/src/Artist/History/ArtistHistoryContentConnector.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchArtistHistory, clearArtistHistory, artistHistoryMarkAsFailed } from 'Store/Actions/artistHistoryActions'; -import ArtistHistoryModalContent from './ArtistHistoryModalContent'; function createMapStateToProps() { return createSelector( @@ -20,20 +19,20 @@ const mapDispatchToProps = { artistHistoryMarkAsFailed }; -class ArtistHistoryModalContentConnector extends Component { +class ArtistHistoryContentConnector extends Component { // // Lifecycle componentDidMount() { const { - artistId, - albumId + authorId, + bookId } = this.props; this.props.fetchArtistHistory({ - artistId, - albumId + authorId, + bookId }); } @@ -46,14 +45,14 @@ class ArtistHistoryModalContentConnector extends Component { onMarkAsFailedPress = (historyId) => { const { - artistId, - albumId + authorId, + bookId } = this.props; this.props.artistHistoryMarkAsFailed({ historyId, - artistId, - albumId + authorId, + bookId }); } @@ -61,21 +60,27 @@ class ArtistHistoryModalContentConnector extends Component { // Render render() { + const { + component: ViewComponent, + ...otherProps + } = this.props; + return ( - ); } } -ArtistHistoryModalContentConnector.propTypes = { - artistId: PropTypes.number.isRequired, - albumId: PropTypes.number, +ArtistHistoryContentConnector.propTypes = { + component: PropTypes.elementType.isRequired, + authorId: PropTypes.number.isRequired, + bookId: PropTypes.number, fetchArtistHistory: PropTypes.func.isRequired, clearArtistHistory: PropTypes.func.isRequired, artistHistoryMarkAsFailed: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, mapDispatchToProps)(ArtistHistoryModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistHistoryContentConnector); diff --git a/frontend/src/Artist/History/ArtistHistoryModal.js b/frontend/src/Artist/History/ArtistHistoryModal.js index 7139d7633..ba6d61aa9 100644 --- a/frontend/src/Artist/History/ArtistHistoryModal.js +++ b/frontend/src/Artist/History/ArtistHistoryModal.js @@ -1,7 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import Modal from 'Components/Modal/Modal'; -import ArtistHistoryModalContentConnector from './ArtistHistoryModalContentConnector'; +import ArtistHistoryContentConnector from './ArtistHistoryContentConnector'; +import ArtistHistoryModalContent from './ArtistHistoryModalContent'; function ArtistHistoryModal(props) { const { @@ -15,7 +16,8 @@ function ArtistHistoryModal(props) { isOpen={isOpen} onModalClose={onModalClose} > - diff --git a/frontend/src/Artist/History/ArtistHistoryModalContent.js b/frontend/src/Artist/History/ArtistHistoryModalContent.js index 9be74ba40..44a76389d 100644 --- a/frontend/src/Artist/History/ArtistHistoryModalContent.js +++ b/frontend/src/Artist/History/ArtistHistoryModalContent.js @@ -1,51 +1,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import ModalContent from 'Components/Modal/ModalContent'; import ModalHeader from 'Components/Modal/ModalHeader'; import ModalBody from 'Components/Modal/ModalBody'; import ModalFooter from 'Components/Modal/ModalFooter'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import ArtistHistoryRowConnector from './ArtistHistoryRowConnector'; - -const columns = [ - { - name: 'eventType', - isVisible: true - }, - { - name: 'album', - label: 'Album', - isVisible: true - }, - { - name: 'sourceTitle', - label: 'Source Title', - isVisible: true - }, - { - name: 'quality', - label: 'Quality', - isVisible: true - }, - { - name: 'date', - label: 'Date', - isVisible: true - }, - { - name: 'details', - label: 'Details', - isVisible: true - }, - { - name: 'actions', - label: 'Actions', - isVisible: true - } -]; +import ArtistHistoryTableContent from './ArtistHistoryTableContent'; class ArtistHistoryModalContent extends Component { @@ -54,18 +14,9 @@ class ArtistHistoryModalContent extends Component { render() { const { - albumId, - isFetching, - isPopulated, - error, - items, - onMarkAsFailedPress, onModalClose } = this.props; - const fullArtist = albumId == null; - const hasItems = !!items.length; - return ( @@ -73,40 +24,9 @@ class ArtistHistoryModalContent extends Component { - { - isFetching && - - } - - { - !isFetching && !!error && -
Unable to load history.
- } - - { - isPopulated && !hasItems && !error && -
No history.
- } - - { - isPopulated && hasItems && !error && - - - { - items.map((item) => { - return ( - - ); - }) - } - -
- } +
@@ -120,12 +40,6 @@ class ArtistHistoryModalContent extends Component { } ArtistHistoryModalContent.propTypes = { - albumId: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onMarkAsFailedPress: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Artist/History/ArtistHistoryTable.js b/frontend/src/Artist/History/ArtistHistoryTable.js new file mode 100644 index 000000000..4709c35c1 --- /dev/null +++ b/frontend/src/Artist/History/ArtistHistoryTable.js @@ -0,0 +1,21 @@ +import React from 'react'; +import ArtistHistoryContentConnector from 'Artist/History/ArtistHistoryContentConnector'; +import ArtistHistoryTableContent from 'Artist/History/ArtistHistoryTableContent'; + +function ArtistHistoryTable(props) { + const { + ...otherProps + } = props; + + return ( + + ); +} + +ArtistHistoryTable.propTypes = { +}; + +export default ArtistHistoryTable; diff --git a/frontend/src/Artist/History/ArtistHistoryTableContent.js b/frontend/src/Artist/History/ArtistHistoryTableContent.js new file mode 100644 index 000000000..03ec5eea6 --- /dev/null +++ b/frontend/src/Artist/History/ArtistHistoryTableContent.js @@ -0,0 +1,113 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import ArtistHistoryRowConnector from './ArtistHistoryRowConnector'; + +const columns = [ + { + name: 'eventType', + isVisible: true + }, + { + name: 'album', + label: 'Album', + isVisible: true + }, + { + name: 'sourceTitle', + label: 'Source Title', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'date', + label: 'Date', + isVisible: true + }, + { + name: 'details', + label: 'Details', + isVisible: true + }, + { + name: 'actions', + label: 'Actions', + isVisible: true + } +]; + +class ArtistHistoryTableContent extends Component { + + // + // Render + + render() { + const { + bookId, + isFetching, + isPopulated, + error, + items, + onMarkAsFailedPress + } = this.props; + + const fullArtist = bookId == null; + const hasItems = !!items.length; + + return ( + <> + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to load history.
+ } + + { + isPopulated && !hasItems && !error && +
No history.
+ } + + { + isPopulated && hasItems && !error && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } + + ); + } +} + +ArtistHistoryTableContent.propTypes = { + bookId: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onMarkAsFailedPress: PropTypes.func.isRequired +}; + +export default ArtistHistoryTableContent; diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.js b/frontend/src/Artist/Index/ArtistIndexFooter.js index 245312ae6..6744d0964 100644 --- a/frontend/src/Artist/Index/ArtistIndexFooter.js +++ b/frontend/src/Artist/Index/ArtistIndexFooter.js @@ -60,7 +60,7 @@ class ArtistIndexFooter extends PureComponent { enableColorImpairedMode && 'colorImpaired' )} /> -
Continuing (All tracks downloaded)
+
Continuing (All books downloaded)
@@ -70,7 +70,7 @@ class ArtistIndexFooter extends PureComponent { enableColorImpairedMode && 'colorImpaired' )} /> -
Ended (All tracks downloaded)
+
Ended (All books downloaded)
@@ -80,7 +80,7 @@ class ArtistIndexFooter extends PureComponent { enableColorImpairedMode && 'colorImpaired' )} /> -
Missing Tracks (Artist monitored)
+
Missing Books (Author monitored)
@@ -90,14 +90,14 @@ class ArtistIndexFooter extends PureComponent { enableColorImpairedMode && 'colorImpaired' )} /> -
Missing Tracks (Artist not monitored)
+
Missing Books (Author not monitored)
@@ -126,7 +126,7 @@ class ArtistIndexFooter extends PureComponent { diff --git a/frontend/src/Artist/Index/ArtistIndexItemConnector.js b/frontend/src/Artist/Index/ArtistIndexItemConnector.js index aef6a8e5e..dd3dff70c 100644 --- a/frontend/src/Artist/Index/ArtistIndexItemConnector.js +++ b/frontend/src/Artist/Index/ArtistIndexItemConnector.js @@ -58,14 +58,14 @@ function createMapStateToProps() { const isRefreshingArtist = executingCommands.some((command) => { return ( command.name === commandNames.REFRESH_ARTIST && - command.body.artistId === artist.id + command.body.authorId === artist.id ); }); const isSearchingArtist = executingCommands.some((command) => { return ( command.name === commandNames.ARTIST_SEARCH && - command.body.artistId === artist.id + command.body.authorId === artist.id ); }); @@ -96,14 +96,14 @@ class ArtistIndexItemConnector extends Component { onRefreshArtistPress = () => { this.props.dispatchExecuteCommand({ name: commandNames.REFRESH_ARTIST, - artistId: this.props.id + authorId: this.props.id }); } onSearchPress = () => { this.props.dispatchExecuteCommand({ name: commandNames.ARTIST_SEARCH, - artistId: this.props.id + authorId: this.props.id }); } diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css index 3f9bfdd8b..223fbe34b 100644 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css @@ -63,7 +63,7 @@ $hoverScale: 1.05; left: 10px; z-index: 3; border-radius: 4px; - background-color: #216044; + background-color: $themeLightColor; color: $white; font-size: $smallFontSize; opacity: 0; diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js index a343942ec..71ad39fe1 100644 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js @@ -58,7 +58,7 @@ class ArtistIndexBanner extends Component { artistName, monitored, status, - foreignArtistId, + titleSlug, nextAiring, statistics, images, @@ -93,7 +93,7 @@ class ArtistIndexBanner extends Component { isDeleteArtistModalOpen } = this.state; - const link = `/artist/${foreignArtistId}`; + const link = `/author/${titleSlug}`; const elementStyle = { width: `${bannerWidth}px`, @@ -216,14 +216,14 @@ class ArtistIndexBanner extends Component {
@@ -237,7 +237,7 @@ ArtistIndexBanner.propTypes = { artistName: PropTypes.string.isRequired, monitored: PropTypes.bool.isRequired, status: PropTypes.string.isRequired, - foreignArtistId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, nextAiring: PropTypes.string, statistics: PropTypes.object.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js index b0c67a174..2e0f2fad2 100644 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js @@ -228,7 +228,7 @@ class ArtistIndexBanners extends Component { showRelativeDates={showRelativeDates} shortDateFormat={shortDateFormat} timeFormat={timeFormat} - artistId={artist.id} + authorId={artist.id} qualityProfileId={artist.qualityProfileId} metadataProfileId={artist.metadataProfileId} /> diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js index d32d33323..c9567f033 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js @@ -75,7 +75,7 @@ class ArtistIndexOverview extends Component { overview, monitored, status, - foreignArtistId, + titleSlug, nextAiring, statistics, images, @@ -110,7 +110,7 @@ class ArtistIndexOverview extends Component { isDeleteArtistModalOpen } = this.state; - const link = `/artist/${foreignArtistId}`; + const link = `/author/${titleSlug}`; const elementStyle = { width: `${posterWidth}px`, @@ -228,14 +228,14 @@ class ArtistIndexOverview extends Component {
@@ -249,7 +249,7 @@ ArtistIndexOverview.propTypes = { overview: PropTypes.string.isRequired, monitored: PropTypes.bool.isRequired, status: PropTypes.string.isRequired, - foreignArtistId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, nextAiring: PropTypes.string, statistics: PropTypes.object.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js index ccd23755f..2260f272f 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js @@ -172,7 +172,7 @@ class ArtistIndexOverviews extends Component { longDateFormat={longDateFormat} timeFormat={timeFormat} isSmallScreen={isSmallScreen} - artistId={artist.id} + authorId={artist.id} qualityProfileId={artist.qualityProfileId} metadataProfileId={artist.metadataProfileId} /> diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css index cd378e34c..4ca1179c7 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css @@ -81,7 +81,7 @@ $hoverScale: 1.05; left: 10px; z-index: 3; border-radius: 4px; - background-color: #216044; + background-color: $themeLightColor; color: $white; font-size: $smallFontSize; opacity: 0; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js index 107462ff5..5c749b33b 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js @@ -70,7 +70,7 @@ class ArtistIndexPoster extends Component { id, artistName, monitored, - foreignArtistId, + titleSlug, status, nextAiring, statistics, @@ -107,12 +107,13 @@ class ArtistIndexPoster extends Component { isDeleteArtistModalOpen } = this.state; - const link = `/artist/${foreignArtistId}`; + const link = `/author/${titleSlug}`; const elementStyle = { width: `${posterWidth}px`, height: `${posterHeight}px` }; + elementStyle['object-fit'] = 'contain'; return (
@@ -239,14 +240,14 @@ class ArtistIndexPoster extends Component {
@@ -260,7 +261,7 @@ ArtistIndexPoster.propTypes = { artistName: PropTypes.string.isRequired, monitored: PropTypes.bool.isRequired, status: PropTypes.string.isRequired, - foreignArtistId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, nextAiring: PropTypes.string, statistics: PropTypes.object.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js index 1bada1b67..bb9a45e5d 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js @@ -204,8 +204,8 @@ class ArtistIndexPosters extends Component { showQualityProfile } = posterOptions; - const artistIdx = rowIndex * columnCount + columnIndex; - const artist = items[artistIdx]; + const authorIdx = rowIndex * columnCount + columnIndex; + const artist = items[authorIdx]; if (!artist) { return null; @@ -229,7 +229,7 @@ class ArtistIndexPosters extends Component { showRelativeDates={showRelativeDates} shortDateFormat={shortDateFormat} timeFormat={timeFormat} - artistId={artist.id} + authorId={artist.id} qualityProfileId={artist.qualityProfileId} metadataProfileId={artist.metadataProfileId} /> diff --git a/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js b/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js index 3f37cd56a..768cb9d29 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js +++ b/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js @@ -78,14 +78,14 @@ class ArtistIndexActionsCell extends Component { diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.js b/frontend/src/Artist/Index/Table/ArtistIndexRow.js index 6acf8a5b9..fab6194b4 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.js +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.js @@ -81,7 +81,7 @@ class ArtistIndexRow extends Component { monitored, status, artistName, - foreignArtistId, + titleSlug, artistType, qualityProfile, metadataProfile, @@ -157,7 +157,7 @@ class ArtistIndexRow extends Component { showBanners ? : } @@ -228,7 +228,7 @@ class ArtistIndexRow extends Component { ); @@ -253,7 +253,7 @@ class ArtistIndexRow extends Component { ); @@ -423,14 +423,14 @@ class ArtistIndexRow extends Component { @@ -443,7 +443,7 @@ ArtistIndexRow.propTypes = { monitored: PropTypes.bool.isRequired, status: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired, - foreignArtistId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, artistType: PropTypes.string, qualityProfile: PropTypes.object.isRequired, metadataProfile: PropTypes.object.isRequired, diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.js b/frontend/src/Artist/Index/Table/ArtistIndexTable.js index 6ce2a761a..426c61a59 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexTable.js +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.js @@ -62,7 +62,7 @@ class ArtistIndexTable extends Component { component={ArtistIndexRow} style={style} columns={columns} - artistId={artist.id} + authorId={artist.id} qualityProfileId={artist.qualityProfileId} metadataProfileId={artist.metadataProfileId} showBanners={showBanners} diff --git a/frontend/src/Artist/NoArtist.js b/frontend/src/Artist/NoArtist.js index 76c2336bc..0615fe058 100644 --- a/frontend/src/Artist/NoArtist.js +++ b/frontend/src/Artist/NoArtist.js @@ -11,7 +11,7 @@ function NoArtist(props) { return (
- All artists are hidden due to the applied filter. + All authors are hidden due to the applied filter.
); @@ -20,7 +20,7 @@ function NoArtist(props) { return (
- No artists found, to get started you'll want to add a new artist or album or add an existing library location (Root Folder) and update. + No authors found, to get started you'll want to add a new author or book or add an existing library location (Root Folder) and update.
@@ -37,7 +37,7 @@ function NoArtist(props) { to="/add/search" kind={kinds.PRIMARY} > - Add New Artist + Add New Author
diff --git a/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js b/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js deleted file mode 100644 index 0da3661a8..000000000 --- a/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ArtistInteractiveSearchModalContent from './ArtistInteractiveSearchModalContent'; - -function ArtistInteractiveSearchModal(props) { - const { - isOpen, - artistId, - onModalClose - } = props; - - return ( - - - - ); -} - -ArtistInteractiveSearchModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - artistId: PropTypes.number.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default ArtistInteractiveSearchModal; diff --git a/frontend/src/Artist/Search/ArtistInteractiveSearchModalConnector.js b/frontend/src/Artist/Search/ArtistInteractiveSearchModalConnector.js deleted file mode 100644 index fe3170570..000000000 --- a/frontend/src/Artist/Search/ArtistInteractiveSearchModalConnector.js +++ /dev/null @@ -1,15 +0,0 @@ -import { connect } from 'react-redux'; -import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; -import ArtistInteractiveSearchModal from './ArtistInteractiveSearchModal'; - -function createMapDispatchToProps(dispatch, props) { - return { - onModalClose() { - dispatch(cancelFetchReleases()); - dispatch(clearReleases()); - props.onModalClose(); - } - }; -} - -export default connect(null, createMapDispatchToProps)(ArtistInteractiveSearchModal); diff --git a/frontend/src/Artist/Search/ArtistInteractiveSearchModalContent.js b/frontend/src/Artist/Search/ArtistInteractiveSearchModalContent.js deleted file mode 100644 index 9b7f4c6ed..000000000 --- a/frontend/src/Artist/Search/ArtistInteractiveSearchModalContent.js +++ /dev/null @@ -1,45 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Button from 'Components/Link/Button'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector'; - -function ArtistInteractiveSearchModalContent(props) { - const { - artistId, - onModalClose - } = props; - - return ( - - - Interactive Search - - - - - - - - - - - ); -} - -ArtistInteractiveSearchModalContent.propTypes = { - artistId: PropTypes.number.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default ArtistInteractiveSearchModalContent; diff --git a/frontend/src/Calendar/Agenda/Agenda.js b/frontend/src/Calendar/Agenda/Agenda.js index 33d02cd79..0a083ba90 100644 --- a/frontend/src/Calendar/Agenda/Agenda.js +++ b/frontend/src/Calendar/Agenda/Agenda.js @@ -20,7 +20,7 @@ function Agenda(props) { return ( diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.js b/frontend/src/Calendar/Agenda/AgendaEvent.js index 44ad53063..316ff3d59 100644 --- a/frontend/src/Calendar/Agenda/AgendaEvent.js +++ b/frontend/src/Calendar/Agenda/AgendaEvent.js @@ -41,7 +41,7 @@ class AgendaEvent extends Component { id, artist, title, - foreignAlbumId, + titleSlug, releaseDate, monitored, statistics, @@ -86,7 +86,7 @@ class AgendaEvent extends Component {
- + {artist.artistName}
@@ -94,7 +94,7 @@ class AgendaEvent extends Component {
-
- + {title}
@@ -123,7 +123,7 @@ AgendaEvent.propTypes = { id: PropTypes.number.isRequired, artist: PropTypes.object.isRequired, title: PropTypes.string.isRequired, - foreignAlbumId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, albumType: PropTypes.string.isRequired, releaseDate: PropTypes.string.isRequired, monitored: PropTypes.bool.isRequired, diff --git a/frontend/src/Calendar/CalendarConnector.js b/frontend/src/Calendar/CalendarConnector.js index a97589c59..41156bb7a 100644 --- a/frontend/src/Calendar/CalendarConnector.js +++ b/frontend/src/Calendar/CalendarConnector.js @@ -65,11 +65,11 @@ class CalendarConnector extends Component { } = this.props; if (hasDifferentItems(prevProps.items, items)) { - const albumIds = selectUniqueIds(items, 'id'); + const bookIds = selectUniqueIds(items, 'id'); // const trackFileIds = selectUniqueIds(items, 'trackFileId'); if (items.length) { - this.props.fetchQueueDetails({ albumIds }); + this.props.fetchQueueDetails({ bookIds }); } // if (trackFileIds.length) { diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js index 9dfe3229e..cd2e24462 100644 --- a/frontend/src/Calendar/CalendarPage.js +++ b/frontend/src/Calendar/CalendarPage.js @@ -61,11 +61,11 @@ class CalendarPage extends Component { onSearchMissingPress = () => { const { - missingAlbumIds, + missingBookIds, onSearchMissingPress } = this.props; - onSearchMissingPress(missingAlbumIds); + onSearchMissingPress(missingBookIds); } // @@ -77,7 +77,7 @@ class CalendarPage extends Component { filters, hasArtist, artistError, - missingAlbumIds, + missingBookIds, isSearchingForMissing, useCurrentPage, onFilterSelect @@ -105,7 +105,7 @@ class CalendarPage extends Component { @@ -182,7 +182,7 @@ CalendarPage.propTypes = { filters: PropTypes.arrayOf(PropTypes.object).isRequired, hasArtist: PropTypes.bool.isRequired, artistError: PropTypes.object, - missingAlbumIds: PropTypes.arrayOf(PropTypes.number).isRequired, + missingBookIds: PropTypes.arrayOf(PropTypes.number).isRequired, isSearchingForMissing: PropTypes.bool.isRequired, useCurrentPage: PropTypes.bool.isRequired, onSearchMissingPress: PropTypes.func.isRequired, diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js index db0f827c1..19affd45b 100644 --- a/frontend/src/Calendar/CalendarPageConnector.js +++ b/frontend/src/Calendar/CalendarPageConnector.js @@ -10,7 +10,7 @@ import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import CalendarPage from './CalendarPage'; -function createMissingAlbumIdsSelector() { +function createMissingBookIdsSelector() { return createSelector( (state) => state.calendar.start, (state) => state.calendar.end, @@ -58,14 +58,14 @@ function createMapStateToProps() { (state) => state.calendar.filters, createArtistCountSelector(), createUISettingsSelector(), - createMissingAlbumIdsSelector(), + createMissingBookIdsSelector(), createIsSearchingSelector(), ( selectedFilterKey, filters, artistCount, uiSettings, - missingAlbumIds, + missingBookIds, isSearchingForMissing ) => { return { @@ -74,7 +74,7 @@ function createMapStateToProps() { colorImpairedMode: uiSettings.enableColorImpairedMode, hasArtist: !!artistCount.count, artistError: artistCount.error, - missingAlbumIds, + missingBookIds, isSearchingForMissing }; } @@ -83,8 +83,8 @@ function createMapStateToProps() { function createMapDispatchToProps(dispatch, props) { return { - onSearchMissingPress(albumIds) { - dispatch(searchMissing({ albumIds })); + onSearchMissingPress(bookIds) { + dispatch(searchMissing({ bookIds })); }, onDaysCountChange(dayCount) { dispatch(setCalendarDaysCount({ dayCount })); diff --git a/frontend/src/Calendar/Day/CalendarDay.js b/frontend/src/Calendar/Day/CalendarDay.js index bd196cc5d..60b6a2301 100644 --- a/frontend/src/Calendar/Day/CalendarDay.js +++ b/frontend/src/Calendar/Day/CalendarDay.js @@ -39,7 +39,7 @@ function CalendarDay(props) { return ( diff --git a/frontend/src/Calendar/Events/CalendarEvent.js b/frontend/src/Calendar/Events/CalendarEvent.js index 8f04fd670..f5584caf7 100644 --- a/frontend/src/Calendar/Events/CalendarEvent.js +++ b/frontend/src/Calendar/Events/CalendarEvent.js @@ -45,7 +45,7 @@ class CalendarEvent extends Component { id, artist, title, - foreignAlbumId, + titleSlug, releaseDate, monitored, statistics, @@ -78,7 +78,7 @@ class CalendarEvent extends Component { >
- + {artist.artistName}
@@ -104,7 +104,7 @@ class CalendarEvent extends Component {
- + {title}
@@ -119,7 +119,7 @@ CalendarEvent.propTypes = { id: PropTypes.number.isRequired, artist: PropTypes.object.isRequired, title: PropTypes.string.isRequired, - foreignAlbumId: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, statistics: PropTypes.object.isRequired, releaseDate: PropTypes.string.isRequired, monitored: PropTypes.bool.isRequired, diff --git a/frontend/src/Components/Form/PlaylistInput.js b/frontend/src/Components/Form/PlaylistInput.js index df482e7bb..022367e81 100644 --- a/frontend/src/Components/Form/PlaylistInput.js +++ b/frontend/src/Components/Form/PlaylistInput.js @@ -104,28 +104,28 @@ class PlaylistInput extends Component { { !isPopulated && !isFetching &&
- Authenticate with spotify to retrieve playlists to import. + Authenticate with Goodreads to retrieve bookshelves to import.
} { isPopulated && !isFetching && !user &&
- Could not retrieve data from Spotify. Try re-authenticating. + Could not retrieve data from Goodreads. Try re-authenticating.
} { isPopulated && !isFetching && user && !items.length &&
- No playlists found for Spotify user {user}. + No bookshelves found for Goodreads user {user}.
} { isPopulated && !isFetching && user && !!items.length &&
- Select playlists to import from Spotify user {user}. + Select playlists to import from Goodreads user {user}. - {rating * 10}% + {rating.toFixed(1)} ); } diff --git a/frontend/src/Components/Page/Header/ArtistSearchInput.js b/frontend/src/Components/Page/Header/ArtistSearchInput.js index 9e067229d..8aca2b055 100644 --- a/frontend/src/Components/Page/Header/ArtistSearchInput.js +++ b/frontend/src/Components/Page/Header/ArtistSearchInput.js @@ -88,7 +88,7 @@ class ArtistSearchInput extends Component { goToArtist(item) { this.setState({ value: '' }); - this.props.onGoToArtist(item.item.foreignArtistId); + this.props.onGoToArtist(item.item.titleSlug); } reset() { diff --git a/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js b/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js index dd1102725..c84007b47 100644 --- a/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js +++ b/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js @@ -16,14 +16,14 @@ function createCleanArtistSelector() { artistName, sortName, images, - foreignArtistId, + titleSlug, tags = [] } = artist; return { artistName, sortName, - foreignArtistId, + titleSlug, images, tags: tags.reduce((acc, id) => { const matchingTag = allTags.find((tag) => tag.id === id); @@ -53,8 +53,8 @@ function createMapStateToProps() { function createMapDispatchToProps(dispatch, props) { return { - onGoToArtist(foreignArtistId) { - dispatch(push(`${window.Readarr.urlBase}/artist/${foreignArtistId}`)); + onGoToArtist(titleSlug) { + dispatch(push(`${window.Readarr.urlBase}/author/${titleSlug}`)); }, onGoToAddNewArtist(query) { diff --git a/frontend/src/Components/Page/Header/PageHeader.css b/frontend/src/Components/Page/Header/PageHeader.css index c4dc3f844..f28c9b02a 100644 --- a/frontend/src/Components/Page/Header/PageHeader.css +++ b/frontend/src/Components/Page/Header/PageHeader.css @@ -4,7 +4,7 @@ align-items: center; flex: 0 0 auto; height: $headerHeight; - background-color: $themeAlternateBlue; + background-color: $themeAlternateRed; color: $white; } @@ -41,12 +41,12 @@ composes: link from '~Components/Link/Link.css'; width: 30px; - color: $themeRed; + color: $themeDarkRed; text-align: center; line-height: 60px; &:hover { - color: #9c1f30; + color: $themeDarkColor; } } diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index dd9e27788..c5848280a 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -33,7 +33,7 @@ const links = [ to: '/artisteditor' }, { - title: 'Album Studio', + title: 'Bookshelf', to: '/albumstudio' }, { diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css index dac40927f..c97087cd1 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css @@ -5,7 +5,7 @@ } .isActiveItem { - border-left: 3px solid $themeBlue; + border-left: 3px solid $themeAlternateRed; } .link { @@ -15,7 +15,7 @@ &:hover, &:focus { - color: $themeBlue; + color: $themeRed; text-decoration: none; } } @@ -27,7 +27,7 @@ } .isActiveLink { - color: $themeBlue; + color: $themeRed; } .isActiveParentLink { diff --git a/frontend/src/Components/StarRating.js b/frontend/src/Components/StarRating.js index f895345b4..06c0e7f59 100644 --- a/frontend/src/Components/StarRating.js +++ b/frontend/src/Components/StarRating.js @@ -6,10 +6,10 @@ import styles from './StarRating.css'; function StarRating({ rating, votes, iconSize }) { const starWidth = { - width: `${rating * 10}%` + width: `${rating * 20}%` }; - const helpText = `${rating/2} (${votes} Votes)`; + const helpText = `${rating.toFixed(1)} (${votes} Votes)`; return ( diff --git a/frontend/src/Components/Table/VirtualTable.js b/frontend/src/Components/Table/VirtualTable.js index a9100e46d..b09ac153d 100644 --- a/frontend/src/Components/Table/VirtualTable.js +++ b/frontend/src/Components/Table/VirtualTable.js @@ -190,6 +190,7 @@ VirtualTable.propTypes = { VirtualTable.defaultProps = { className: styles.tableContainer, headerHeight: 38, + rowHeight: 38, onRecompute: () => {} }; diff --git a/frontend/src/Content/Images/Icons/android-chrome-192x192.png b/frontend/src/Content/Images/Icons/android-chrome-192x192.png index 88a584f88cbd9f97b0cca8793d525b48dccdafe9..1a8b667923789f063bb203e496eb0d043145970e 100644 GIT binary patch literal 20595 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 b7+!?X1o?0q+A1mF5D)|OU%F)NhvEMLW>XyU literal 20949 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%0DE>@;3 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 Hu0mjf5Xi8= diff --git a/frontend/src/Content/Images/Icons/apple-touch-icon.png b/frontend/src/Content/Images/Icons/apple-touch-icon.png index 14aed161682590e986063c91e9a6a25f2684df57..d5b8e81592872cbc0d8ff95811623f947cb856bc 100644 GIT binary patch literal 19102 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 QC;$Ke07*qoM6N<$g5ZZD1poj5 diff --git a/frontend/src/Content/Images/Icons/browserconfig.xml b/frontend/src/Content/Images/Icons/browserconfig.xml index 993924968..b3930d0f0 100644 --- a/frontend/src/Content/Images/Icons/browserconfig.xml +++ b/frontend/src/Content/Images/Icons/browserconfig.xml @@ -2,8 +2,8 @@ - - #00ccff + + #da532c diff --git a/frontend/src/Content/Images/Icons/favicon-16x16.png b/frontend/src/Content/Images/Icons/favicon-16x16.png index eb2a9cf70479eb9e4deda7d4be44df38ccc9728d..8da46619334ac068cfb050a3e9d072c4a38b8343 100644 GIT binary patch literal 1124 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>*kge7oRgkD!`60oY;3ZR&m4RE={7bq?Cd6JXv}tSNb~kSy<)|Y$&=?gILx%Q zooQ`7$Id z1;(#GT9_BE?UDcSTdQ%xk;{)hJ^6C%T3v4+)4E-(-M{}%WmCWO>h%@faK|s8B*B>E z?e4<)@#fMuKn`btM`SUO&H!P?#m}mM34wvV#M9T6{RuNCqb~DdkL5f-p_iU6jv*44 zlM@)2+|<&wYZ7xqqW-n_VbcKhlMDe)}&$Jpq2P;p`6L&1rG7Y&nm9&IfA`0*oW zrzfX_mZfWxqme1^i7x;13=?NY2Ju{4c~c?HPp{+5=}Czbde5>Oavxx7)hy!q^v_ix zt&p)pr!$ZtDmseIWc?l`yM3EhNoD9Zuh{GCn;To}oE*Hld#=)jrCz7fp6Ra5eJiWG ztxRi`!ub!YRyx}(U$~O>>-uf8`qs_cH-oW*m%%Z{&SSApRRPe`swJ)wB`Jv|saDBF zsX&Us$iT=z*T7iUz%sD9TUE%t=)!sVqoU z$Sf#HW?-n8^Y{}FM`4(T#wq{PXFQ(m_pwD+_y17GV}vaA`0(oWiWUIYi;~ pjVmXPoH-(Mg#C1b#{w@shF9W(C7+y3rvj~D@O1TaS?83{1OR-Y4|D(k delta 662 zcmV;H0%`r^2(1N>BYy#gP)t-s0002x<>lq&<>lq&=;!F_>FVF*;@{=tlq&=I7?<=jiI`>FMd|=I7_h)z6%;r)QpqY?XtYucyh?(0-SU zCpj?;As7ZaBMl@P4HOY3Ix&8hj9z$b3=|MFH8dqfHxw=-iGQ4vhJ=MSH8oy%Z449< zTUuJ3vZ)?3Di|ptztG6;>+I3c&{|ko4HFQ3m5V1jGBq?c$IZ%Ab!G?@4-iN%g}tT0 z)y%}z&8oY!B{eQ3KQzhI&zi5M4HXiGg@&EDtPm<61Y|k@kWmGdRuzO(9X&4|GARu? zCJQARny#gMlz)j46cd-Do;+Ag1u-4~k5K@UQUH!m2r(ZH6cI#YRT35yeUyq47ZgQg zR{@Yw1~MO(qMZ#V8xTAx5i1{uhKD9VG$uAL#MRD)y`&IFFI07B$IZ((H8v+ZGFn(z z(9h5xG%KC6sasiEf0vDkoRkzVBPK>TG&MB{J0%Pv8GnA3i<_^eZI*;+pNE^Ur&#DF z0RR916?9TgQvd)3K%U(7^%RNgsQ&&0LFt{_^yyFNIQIYm0KrK_K~xwS1;H^c1YrOM z(EsiJn;|PBThIxege&ND3cVw^j}xfG1>8U>k%&S^AwprzEV7pUyr&evGD%V>4P!7( z=7nZx9e?n){F`eRbrbN@JpPTW8&3duh{@^}0Nhl206^6O1yIWxizH=5-%iJ6kbX3} z>-#=0wX5B82I*>@Y;`*uU9-Egf1kR%?Q>vuokOmUzM@?Wx2=5~<1(+mqd}iMz32O4 wI6OajU8birj8Ad)naqn)(9Q-)lBE0tf=5=v2oK3~CIA2c07*qoM6N<$f}}zx^8f$< diff --git a/frontend/src/Content/Images/Icons/favicon-32x32.png b/frontend/src/Content/Images/Icons/favicon-32x32.png index 242d170fb8260df0c1c5d6c467cb09032bb219c4..d1c5e269e2c2ce2823fc1d1f6aa8e1d99c20eecd 100644 GIT binary patch literal 2526 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 WT(IPmlj&5T6%3xPelF{r5}E*D)HN;u delta 658 zcmV;D0&V^N2%iOz8Gi-<001BJ|6u?C0%l1>K~#7FbyG9I9AOmPsEwQ0{B7GjBeu;Z ztYWi?ZQHhu-#l%cHj_6o_NzGzW#nS5Q!N1J`aGgvlI_ zUVUQmxZUBUHMty&GP%eBc>C-N65%M| z(2;FHL4Ox69)KrL?~27n*hO#(PXm70NNZ@(D6xu`(K=d4D~;wzT)4Cqw3t@!+&%A? zjT%An0qtaX7$Tj)4+96s$dz@PLDGyeGeK~g#JG3=7D!3!1jbt<%HGR#=G>NVMLKd~ s42$KxOkV1|mKA1|S!sTDd8zYPXWVs)d$5dF01E&B07*qoM6N<$g8gtkO#lD@ diff --git a/frontend/src/Content/Images/Icons/favicon-debug-32x32.png b/frontend/src/Content/Images/Icons/favicon-debug-32x32.png index 363966ffa712290ad12df0e716e38274954346bc..5edb153624eac4e33cfe900f77911b3ebc955c8a 100644 GIT binary patch delta 2539 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 delta 1931 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 diff --git a/frontend/src/Content/Images/Icons/favicon.ico b/frontend/src/Content/Images/Icons/favicon.ico index 1b0de8423b7621792938d016fe0fa45fca532b7d..de91af5972a0e79bc53de30a11dc49b94c266c32 100644 GIT binary patch literal 15086 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 literal 15086 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 diff --git a/frontend/src/Content/Images/Icons/mstile-144x144.png b/frontend/src/Content/Images/Icons/mstile-144x144.png index 1ffe2e9c5c6ac9a6b436c0e560e011cc36a72b5c..e1a6c42db755b476ec0c7cf57e516e23bde94571 100644 GIT binary patch literal 14922 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 diff --git a/frontend/src/Content/Images/Icons/mstile-150x150.png b/frontend/src/Content/Images/Icons/mstile-150x150.png index 1008bf9a0a38fd268e98110d917ac3e953e77281..2178a97fa87e9abbc55d4a9965070f2c94a5faf1 100644 GIT binary patch literal 13845 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 HgMj}JCW}Hb literal 14626 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 diff --git a/frontend/src/Content/Images/Icons/mstile-310x310.png b/frontend/src/Content/Images/Icons/mstile-310x310.png index 6e62ce87f8951a9a7dfe3571e7bc0dccb84f903a..3ea04ad6f9d1a53246e32223cdaa916e62e9cced 100644 GIT binary patch literal 32047 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 Pfg&%XEL|dL6!gCU6p;M9 literal 35066 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�NnCF(N@A?qz)1jUb*t&x^IPGokwuGAa^g-F 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ⅇ-vUbJ5pz3Ddj zHd{A)b^F&CKhH6s&sGxvZ`xqHV>AEZH!$SQ-#8+XmyXLzzWhPe5`H$udxT^T@2kEh z0PYqu?Gh)^^dEi+0QkV&CKKrg_plybOKRjIAQ7moeK;Y)1 z-~W`cyHJ3ZEMHKjrjy5&ioR#;pl}4~u7A9V!hh0gzTUB - + + diff --git a/frontend/src/Content/Images/logo.svg b/frontend/src/Content/Images/logo.svg index ebebe49a9..b8ec609ba 100644 --- a/frontend/src/Content/Images/logo.svg +++ b/frontend/src/Content/Images/logo.svg @@ -1 +1,23 @@ - background Layer 1 \ No newline at end of file + + + + + background + + + + + + + Layer 1 + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/Content/Images/poster-dark-square.png b/frontend/src/Content/Images/poster-dark-square.png index efadba25eabc676c6286ef532c5044408e453997..9a0299771645bd8858beb43943d6c890e0d90b97 100644 GIT binary patch delta 1112 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 Y$0gNIN&dNK`(7*peKBb6#h8Ns0Qpmi3jhEB diff --git a/frontend/src/Content/Images/poster-dark.png b/frontend/src/Content/Images/poster-dark.png index 0b5c9786aa19db92e304d8b26b991f1e06b45695..1407a949db786981c01adb5cb5bb03f01dd3f447 100644 GIT binary patch delta 1065 zcmV+^1lIf346+E27=Hl+00029U5(WM000J1OjJcDCnq5xAqca=uF-o1_#g-(d3yHrDzUb~j_?YgmenkFWs?TMJ8zh{b}95d{~F z8PC^dl1VNof+FmJ-I@IEnKS2PW@r8v&UUu5{R4Y&_r(%k$A8bA1$~Ae>mqoYzi-Z= z(PmDqw)bZALfd>sD{Ye~teS&0E8{Zu_woD2JZ$2X?4pB~Q5l&`r*l$)6Lg>2GOj-F$oB5q0P`jJw)@#{rIWfyYNLw(FrjA~jG9 zRG8RDAX-s1NR7b3;bV`~GKE{31JhF8hdK|Ahayd(?!6m-o7eoFoKj9oenl+}(HHE$ zp%*_pNq-wz&eXRE1|IOqldtgy8pazBJLu!!M17O_HXCVf{A>QXC%!4D^M!pAp@Z?R z7TPddx>$4M0VSA#-%OX(7ln$4*(iWi1bfmwOrt_^;sut*Gjlz<47OKrK zr!B6?l6LhK)Lcc2IIhyz_h$So1O(Ly+k;)4FIrS^M z1XlKr1;}Dw-jlyapU&ueK8=$FZPH(*AC$h1DFB$5>TmuqqX(pD3mQ%sH92AORZ3QW zpD;bS(yOSCDbVQ|(H_cG3Un$*luqS!3T_ioIqlbJ7E!)MswAR%55?3HH4&AY%=jM@ zwtx37)f=)1$?^>;7pMut^)}6+_7feYN+3l<_3`q(p<8-;y3tK7biC0m-~KzMUq^jl zxR)_)*`rcb(Su>?a7=R&M>KDZlpAM)M1cb7QMy6dVBlB2oynOUJtK9Zf~JZI=~OX+ zsH@$fpQ@`=lvr`-rb^6;YRe9-sJ3#d@PCpIjVL@7pH9@^6H$ZUCd#orDMvGwp(=Av zTf{2e@wrp)_4px;9e9YYN&0X&=UqZ|HrLF};Q{o+81* jZohaAXFJ>3{=WSL?{=xcmI5;@qI>v7R00sp~ zL_t(|+U?!pa@;lyg;DCm`@eH1b!rTaP?r#BE1kjp)3H~ZoLq|QwN_+Dr_2x}sPN&o9bUK~?bNGn;8$R;)5$#&UfWO6Pmw$*w?fXs2X5UxpykX5M zz1N>ra_PKZt1Q!0t?x2D=riC|rZaYqd5`G^hrUGR+R64*yy%a1vyH=iHS@sT|&KQrw1Y5dRWQ0HQm;jhnjb7X)fti~y) zX1dd%!D{x6Qh)B45i=Yba!2KxoMx_(P))5%*=M>C#yU=Pzt_5`SNH*zT0Na}mBi3gRz6CpGEB z4~=cJ|H1=S7<_b}IPJWreLS4$nX==$r9YXxUm46s6kP)tD8Ok#?*rpMk3UA=8$S*RaDjDOvS0e7!Zq(N$!#tLG?Q=(9$VP)*qqBNPky1zm$J6h39h!Iym4a<}rQ*#Fk0=y9@_OtOm3mq-fOFQGoLtY;E>D&@zX=Zyjf zZ`Ck$r?{mSrlV#Od>)s(my87zF@N)QqhN*GiR95ns3rjo(CQ6Zx>80$3R$O~O1jbr zr4A)gXl^=9X8;VJz`^x(57 zUqgXdryltF!YfUMDDgH?PWz^%Y;X$&n>*sYMRxx+fUcN0#Tn7}+^UKyN3)q40U0`pp4+UhM+Nr~h zls#x_%RS@xW)*vlLV2@_`hWHx8&E*hshoAGK?17k6wdM%HE;_BG}FV6ZTb_TV$g-N zJok00@P|UVV;VoVF%|wF7!2_5IkV`jZ}r93Mj^4)*H3Sr2A|O1UlcY^D@Zlg!EbCw z_x=Wd^-}AbCi2cEln%A))tx=rhGyz~uT-!^oi9?c1oKTQ3FfO*lS<6qaQfsH-n}XM zA*%tsasP>C4e0;5M5oi~bUK|*r_2x}s&aeCfc3M8KcM3(- P00000NkvXXu0mjfl;9WN diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js index 20115214e..7b725c0c9 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js @@ -17,17 +17,14 @@ import styles from './SelectAlbumModalContent.css'; const columns = [ { name: 'title', - label: 'Album Title', - isVisible: true - }, - { - name: 'albumType', - label: 'Album Type', + label: 'Book Title', + isSortable: true, isVisible: true }, { name: 'releaseDate', label: 'Release Date', + isSortable: true, isVisible: true }, { @@ -74,7 +71,7 @@ class SelectAlbumModalContent extends Component { return ( - Manual Import - Select Album + Manual Import - Select Book { - const album = _.find(this.props.items, { id: albumId }); + onAlbumSelect = (bookId) => { + const album = _.find(this.props.items, { id: bookId }); const ids = this.props.ids; @@ -83,6 +83,7 @@ class SelectAlbumModalContentConnector extends Component { return ( ); @@ -91,7 +92,7 @@ class SelectAlbumModalContentConnector extends Component { SelectAlbumModalContentConnector.propTypes = { ids: PropTypes.arrayOf(PropTypes.number).isRequired, - artistId: PropTypes.number.isRequired, + authorId: PropTypes.number.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, fetchInteractiveImportAlbums: PropTypes.func.isRequired, setInteractiveImportAlbumsSort: PropTypes.func.isRequired, diff --git a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModal.js b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModal.js deleted file mode 100644 index f3789d9dd..000000000 --- a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModal.js +++ /dev/null @@ -1,37 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Modal from 'Components/Modal/Modal'; -import SelectAlbumReleaseModalContentConnector from './SelectAlbumReleaseModalContentConnector'; - -class SelectAlbumReleaseModal extends Component { - - // - // Render - - render() { - const { - isOpen, - onModalClose, - ...otherProps - } = this.props; - - return ( - - - - ); - } -} - -SelectAlbumReleaseModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectAlbumReleaseModal; diff --git a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContent.css b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContent.css deleted file mode 100644 index 54f67bb07..000000000 --- a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContent.css +++ /dev/null @@ -1,18 +0,0 @@ -.modalBody { - composes: modalBody from '~Components/Modal/ModalBody.css'; - - display: flex; - flex: 1 1 auto; - flex-direction: column; -} - -.filterInput { - composes: input from '~Components/Form/TextInput.css'; - - flex: 0 0 auto; - margin-bottom: 20px; -} - -.scroller { - flex: 1 1 auto; -} diff --git a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContent.js b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContent.js deleted file mode 100644 index 5c87f982e..000000000 --- a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContent.js +++ /dev/null @@ -1,93 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Button from 'Components/Link/Button'; -import { scrollDirections } from 'Helpers/Props'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import SelectAlbumReleaseRow from './SelectAlbumReleaseRow'; -import Alert from 'Components/Alert'; -import styles from './SelectAlbumReleaseModalContent.css'; - -const columns = [ - { - name: 'album', - label: 'Album', - isVisible: true - }, - { - name: 'release', - label: 'Album Release', - isVisible: true - } -]; - -class SelectAlbumReleaseModalContent extends Component { - - // - // Render - - render() { - const { - albums, - onAlbumReleaseSelect, - onModalClose, - ...otherProps - } = this.props; - - return ( - - - Manual Import - Select Album Release - - - - - Overrriding a release here will disable automatic release selection for that album in future. - - -

- - { - albums.map((item) => { - return ( - - ); - }) - } - -
- - - - - - - ); - } -} - -SelectAlbumReleaseModalContent.propTypes = { - albums: PropTypes.arrayOf(PropTypes.object).isRequired, - onAlbumReleaseSelect: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectAlbumReleaseModalContent; diff --git a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContentConnector.js b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContentConnector.js deleted file mode 100644 index f308b03ce..000000000 --- a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseModalContentConnector.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { - updateInteractiveImportItem, - saveInteractiveImportItem -} from 'Store/Actions/interactiveImportActions'; -import SelectAlbumReleaseModalContent from './SelectAlbumReleaseModalContent'; - -function createMapStateToProps() { - return {}; -} - -const mapDispatchToProps = { - updateInteractiveImportItem, - saveInteractiveImportItem -}; - -class SelectAlbumReleaseModalContentConnector extends Component { - - // - // Listeners - - // onSortPress = (sortKey, sortDirection) => { - // this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection }); - // } - - onAlbumReleaseSelect = (albumId, albumReleaseId) => { - const ids = this.props.importIdsByAlbum[albumId]; - - ids.forEach((id) => { - this.props.updateInteractiveImportItem({ - id, - albumReleaseId, - disableReleaseSwitching: true, - tracks: [], - rejections: [] - }); - }); - - this.props.saveInteractiveImportItem({ id: ids }); - - this.props.onModalClose(true); - } - - // - // Render - - render() { - return ( - - ); - } -} - -SelectAlbumReleaseModalContentConnector.propTypes = { - importIdsByAlbum: PropTypes.object.isRequired, - albums: PropTypes.arrayOf(PropTypes.object).isRequired, - updateInteractiveImportItem: PropTypes.func.isRequired, - saveInteractiveImportItem: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(SelectAlbumReleaseModalContentConnector); diff --git a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.css b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.css deleted file mode 100644 index e78f0bc19..000000000 --- a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.css +++ /dev/null @@ -1,3 +0,0 @@ -.albumRow { - cursor: pointer; -} diff --git a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js b/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js deleted file mode 100644 index 786ea0f83..000000000 --- a/frontend/src/InteractiveImport/AlbumRelease/SelectAlbumReleaseRow.js +++ /dev/null @@ -1,96 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { inputTypes } from 'Helpers/Props'; -import TableRow from 'Components/Table/TableRow'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import titleCase from 'Utilities/String/titleCase'; - -class SelectAlbumReleaseRow extends Component { - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.onAlbumReleaseSelect(parseInt(name), parseInt(value)); - } - - // - // Render - - render() { - const { - id, - matchedReleaseId, - title, - disambiguation, - releases, - columns - } = this.props; - - const extendedTitle = disambiguation ? `${title} (${disambiguation})` : title; - - return ( - - { - columns.map((column) => { - const { - name, - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (name === 'album') { - return ( - - {extendedTitle} - - ); - } - - if (name === 'release') { - return ( - - ({ - key: r.id, - value: `${r.title}` + - `${r.disambiguation ? ' (' : ''}${titleCase(r.disambiguation)}${r.disambiguation ? ')' : ''}` + - `, ${r.mediumCount} med, ${r.trackCount} tracks` + - `${r.country.length > 0 ? ', ' : ''}${r.country}` + - `${r.format ? ', [' : ''}${r.format}${r.format ? ']' : ''}` + - `${r.monitored ? ', Monitored' : ''}` - }))} - value={matchedReleaseId} - onChange={this.onInputChange} - /> - - ); - } - - return null; - }) - } - - - ); - } -} - -SelectAlbumReleaseRow.propTypes = { - id: PropTypes.number.isRequired, - matchedReleaseId: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - disambiguation: PropTypes.string.isRequired, - releases: PropTypes.arrayOf(PropTypes.object).isRequired, - onAlbumReleaseSelect: PropTypes.func.isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default SelectAlbumReleaseRow; diff --git a/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js b/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js index a7b72b9d5..c6cd767cb 100644 --- a/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js +++ b/frontend/src/InteractiveImport/Artist/SelectArtistModalContentConnector.js @@ -38,8 +38,8 @@ class SelectArtistModalContentConnector extends Component { // // Listeners - onArtistSelect = (artistId) => { - const artist = _.find(this.props.items, { id: artistId }); + onArtistSelect = (authorId) => { + const artist = _.find(this.props.items, { id: authorId }); const ids = this.props.ids; diff --git a/frontend/src/InteractiveImport/Confirmation/ConfirmImportModalContent.js b/frontend/src/InteractiveImport/Confirmation/ConfirmImportModalContent.js index c91aa333b..39b156d5e 100644 --- a/frontend/src/InteractiveImport/Confirmation/ConfirmImportModalContent.js +++ b/frontend/src/InteractiveImport/Confirmation/ConfirmImportModalContent.js @@ -93,7 +93,7 @@ class ConfirmImportModalContent extends Component { { _.chain(items) - .groupBy('albumId') + .groupBy('bookId') .mapValues((value, key) => formatAlbumFiles(value, _.find(albums, (a) => a.id === parseInt(key)))) .values() .value() } diff --git a/frontend/src/InteractiveImport/Confirmation/ConfirmImportModalContentConnector.js b/frontend/src/InteractiveImport/Confirmation/ConfirmImportModalContentConnector.js index dab76fb33..a21ca74ce 100644 --- a/frontend/src/InteractiveImport/Confirmation/ConfirmImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Confirmation/ConfirmImportModalContentConnector.js @@ -30,7 +30,7 @@ class ConfirmImportModalContentConnector extends Component { albums } = this.props; - this.props.fetchInteractiveImportTrackFiles({ albumId: albums.map((x) => x.id) }); + this.props.fetchInteractiveImportTrackFiles({ bookId: albums.map((x) => x.id) }); } componentWillUnmount() { diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index c84fd567d..11526b16c 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -23,7 +23,6 @@ import TableBody from 'Components/Table/TableBody'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal'; -import SelectAlbumReleaseModal from 'InteractiveImport/AlbumRelease/SelectAlbumReleaseModal'; import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal'; import InteractiveImportRow from './InteractiveImportRow'; import styles from './InteractiveImportModalContent.css'; @@ -43,12 +42,7 @@ const columns = [ }, { name: 'album', - label: 'Album', - isVisible: true - }, - { - name: 'tracks', - label: 'Track(s)', + label: 'Book', isVisible: true }, { @@ -85,7 +79,6 @@ const importModeOptions = [ const SELECT = 'select'; const ARTIST = 'artist'; const ALBUM = 'album'; -const ALBUM_RELEASE = 'albumRelease'; const QUALITY = 'quality'; const replaceExistingFilesOptions = { @@ -110,7 +103,6 @@ class InteractiveImportModalContent extends Component { selectModalOpen: null, albumsImported: [], isConfirmImportModalOpen: false, - showClearTracks: false, inconsistentAlbumReleases: false }; } @@ -118,15 +110,10 @@ class InteractiveImportModalContent extends Component { componentDidUpdate(prevProps) { const selectedIds = this.getSelectedIds(); const selectedItems = _.filter(this.props.items, (x) => _.includes(selectedIds, x.id)); - const selectionHasTracks = _.some(selectedItems, (x) => x.tracks.length); - - if (this.state.showClearTracks !== selectionHasTracks) { - this.setState({ showClearTracks: selectionHasTracks }); - } const inconsistent = _(selectedItems) - .map((x) => ({ albumId: x.album ? x.album.id : 0, releaseId: x.albumReleaseId })) - .groupBy('albumId') + .map((x) => ({ bookId: x.album ? x.album.id : 0, releaseId: x.albumReleaseId })) + .groupBy('bookId') .mapValues((album) => _(album).groupBy((x) => x.releaseId).values().value().length) .values() .some((x) => x !== undefined && x > 1); @@ -224,7 +211,6 @@ class InteractiveImportModalContent extends Component { selectedIds.forEach((id) => { this.props.updateInteractiveImportItem({ id, - tracks: [], rejections: [] }); }); @@ -277,7 +263,6 @@ class InteractiveImportModalContent extends Component { selectModalOpen, albumsImported, isConfirmImportModalOpen, - showClearTracks, inconsistentAlbumReleases } = this.state; @@ -288,7 +273,6 @@ class InteractiveImportModalContent extends Component { const bulkSelectOptions = [ { key: SELECT, value: 'Select...', disabled: true }, { key: ALBUM, value: 'Select Album' }, - { key: ALBUM_RELEASE, value: 'Select Album Release' }, { key: QUALITY, value: 'Select Quality' } ]; @@ -450,24 +434,6 @@ class InteractiveImportModalContent extends Component { isDisabled={!selectedIds.length} onChange={this.onSelectModalSelect} /> - - { - showClearTracks ? ( - - ) : ( - - ) - }
@@ -499,14 +465,7 @@ class InteractiveImportModalContent extends Component { - - x.album).groupBy((x) => x.album.id).mapValues((x) => x.map((y) => y.id)).value()} - albums={_.chain(items).filter((x) => x.album).keyBy((x) => x.album.id).mapValues((x) => ({ matchedReleaseId: x.albumReleaseId, album: x.album })).values().value()} + authorId={selectedItem && selectedItem.artist && selectedItem.artist.id} onModalClose={this.onSelectModalClose} /> diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index 1bf8771ba..7b2240b80 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -124,8 +124,6 @@ class InteractiveImportModalContentConnector extends Component { const { artist, album, - albumReleaseId, - tracks, quality, disableReleaseSwitching } = item; @@ -140,11 +138,6 @@ class InteractiveImportModalContentConnector extends Component { return false; } - if (!tracks || !tracks.length) { - this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' }); - return false; - } - if (!quality) { this.setState({ interactiveImportErrorMessage: 'Quality must be chosen for each selected file' }); return false; @@ -152,10 +145,8 @@ class InteractiveImportModalContentConnector extends Component { files.push({ path: item.path, - artistId: artist.id, - albumId: album.id, - albumReleaseId, - trackIds: _.map(tracks, 'id'), + authorId: artist.id, + bookId: album.id, quality, downloadId: this.props.downloadId, disableReleaseSwitching diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index 0be12c0eb..04fc240e9 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -1,8 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import formatBytes from 'Utilities/Number/formatBytes'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; -import { icons, kinds, tooltipPositions, sortDirections } from 'Helpers/Props'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import Icon from 'Components/Icon'; import TableRow from 'Components/Table/TableRow'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; @@ -13,10 +12,8 @@ import Tooltip from 'Components/Tooltip/Tooltip'; import TrackQuality from 'Album/TrackQuality'; import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal'; -import SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import styles from './InteractiveImportRow.css'; class InteractiveImportRow extends Component { @@ -30,7 +27,6 @@ class InteractiveImportRow extends Component { this.state = { isSelectArtistModalOpen: false, isSelectAlbumModalOpen: false, - isSelectTrackModalOpen: false, isSelectQualityModalOpen: false }; } @@ -40,14 +36,12 @@ class InteractiveImportRow extends Component { id, artist, album, - tracks, quality } = this.props; if ( artist && album != null && - tracks.length && quality ) { this.props.onSelectedChange({ id, value: true }); @@ -59,7 +53,6 @@ class InteractiveImportRow extends Component { id, artist, album, - tracks, quality, isSelected, onValidRowChange @@ -68,7 +61,6 @@ class InteractiveImportRow extends Component { if ( prevProps.artist === artist && prevProps.album === album && - !hasDifferentItems(prevProps.tracks, tracks) && prevProps.quality === quality && prevProps.isSelected === isSelected ) { @@ -78,7 +70,6 @@ class InteractiveImportRow extends Component { const isValid = !!( artist && album && - tracks.length && quality ); @@ -114,10 +105,6 @@ class InteractiveImportRow extends Component { this.setState({ isSelectAlbumModalOpen: true }); } - onSelectTrackPress = () => { - this.setState({ isSelectTrackModalOpen: true }); - } - onSelectQualityPress = () => { this.setState({ isSelectQualityModalOpen: true }); } @@ -132,11 +119,6 @@ class InteractiveImportRow extends Component { this.selectRowAfterChange(changed); } - onSelectTrackModalClose = (changed) => { - this.setState({ isSelectTrackModalOpen: false }); - this.selectRowAfterChange(changed); - } - onSelectQualityModalClose = (changed) => { this.setState({ isSelectQualityModalOpen: false }); this.selectRowAfterChange(changed); @@ -152,22 +134,17 @@ class InteractiveImportRow extends Component { path, artist, album, - albumReleaseId, - tracks, quality, size, rejections, - audioTags, additionalFile, isSelected, - isSaving, onSelectedChange } = this.props; const { isSelectArtistModalOpen, isSelectAlbumModalOpen, - isSelectTrackModalOpen, isSelectQualityModalOpen } = this.state; @@ -177,15 +154,8 @@ class InteractiveImportRow extends Component { albumTitle = album.disambiguation ? `${album.title} (${album.disambiguation})` : album.title; } - const sortedTracks = tracks.sort((a, b) => parseInt(a.absoluteTrackNumber) - parseInt(b.absoluteTrackNumber)); - - const trackNumbers = sortedTracks.map((track) => `${track.mediumNumber}x${track.trackNumber}`) - .join(', '); - const showArtistPlaceholder = isSelected && !artist; const showAlbumNumberPlaceholder = isSelected && !!artist && !album; - const showTrackNumbersPlaceholder = !isSaving && isSelected && !!album && !tracks.length; - const showTrackNumbersLoading = isSaving && !tracks.length; const showQualityPlaceholder = isSelected && !quality; const pathCellContents = ( @@ -239,19 +209,6 @@ class InteractiveImportRow extends Component { } - - { - showTrackNumbersLoading && - } - { - showTrackNumbersPlaceholder ? : trackNumbers - } - - - - - - - ); - } -} - -SelectTrackModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectTrackModal; diff --git a/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js b/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js deleted file mode 100644 index 0934cc047..000000000 --- a/frontend/src/InteractiveImport/Track/SelectTrackModalContent.js +++ /dev/null @@ -1,236 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import _ from 'lodash'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import selectAll from 'Utilities/Table/selectAll'; -import toggleSelected from 'Utilities/Table/toggleSelected'; -import { kinds } from 'Helpers/Props'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import SelectTrackRow from './SelectTrackRow'; -import ExpandingFileDetails from 'TrackFile/ExpandingFileDetails'; - -const columns = [ - { - name: 'mediumNumber', - label: 'Medium', - isSortable: true, - isVisible: true - }, - { - name: 'trackNumber', - label: '#', - isSortable: true, - isVisible: true - }, - { - name: 'title', - label: 'Title', - isVisible: true - }, - { - name: 'trackStatus', - label: 'Status', - isVisible: true - } -]; - -const selectAllBlankColumn = [ - { - name: 'dummy', - label: ' ', - isVisible: true - } -]; - -class SelectTrackModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const selectedTracks = _.filter(props.selectedTracksByItem, ['id', props.id])[0].tracks; - const init = _.zipObject(selectedTracks, _.times(selectedTracks.length, _.constant(true))); - - this.state = { - allSelected: false, - allUnselected: false, - lastToggled: null, - selectedState: init - }; - - props.onSortPress( props.sortKey, props.sortDirection ); - } - - // - // Control - - getSelectedIds = () => { - return getSelectedIds(this.state.selectedState); - } - - // - // Listeners - - onSelectAllChange = ({ value }) => { - this.setState(selectAll(this.state.selectedState, value)); - } - - onSelectedChange = ({ id, value, shiftKey = false }) => { - this.setState((state) => { - return toggleSelected(state, this.props.items, id, value, shiftKey); - }); - } - - onTracksSelect = () => { - this.props.onTracksSelect(this.getSelectedIds()); - } - - // - // Render - - render() { - const { - id, - audioTags, - rejections, - isFetching, - isPopulated, - error, - items, - sortKey, - sortDirection, - onSortPress, - onModalClose, - selectedTracksByItem, - filename - } = this.props; - - const { - allSelected, - allUnselected, - selectedState - } = this.state; - - const errorMessage = getErrorMessage(error, 'Unable to load tracks'); - - // all tracks selected for other items - const otherSelected = _.map(_.filter(selectedTracksByItem, (item) => { - return item.id !== id; - }), (x) => { - return x.tracks; - }).flat(); - // tracks selected for the current file - const currentSelected = _.keys(_.pickBy(selectedState, _.identity)).map(Number); - // only enable selectAll if no other files have any tracks selected. - const selectAllEnabled = otherSelected.length === 0; - - return ( - - - Manual Import - Select Track(s): - - - - { - isFetching && - - } - - { - error && -
{errorMessage}
- } - - - - { - isPopulated && !!items.length && - - - { - items.map((item) => { - return ( - - ); - }) - } - -
- } - - { - isPopulated && !items.length && - 'No tracks were found for the selected album' - } -
- - - - - - -
- ); - } -} - -SelectTrackModalContent.propTypes = { - id: PropTypes.number.isRequired, - rejections: PropTypes.arrayOf(PropTypes.object).isRequired, - audioTags: PropTypes.object.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - sortKey: PropTypes.string, - sortDirection: PropTypes.string, - onSortPress: PropTypes.func.isRequired, - onTracksSelect: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - selectedTracksByItem: PropTypes.arrayOf(PropTypes.object).isRequired, - filename: PropTypes.string.isRequired -}; - -export default SelectTrackModalContent; diff --git a/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js b/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js deleted file mode 100644 index 35b17ade5..000000000 --- a/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js +++ /dev/null @@ -1,112 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchTracks, setTracksSort, clearTracks } from 'Store/Actions/trackActions'; -import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import SelectTrackModalContent from './SelectTrackModalContent'; - -function createMapStateToProps() { - return createSelector( - createClientSideCollectionSelector('tracks'), - createClientSideCollectionSelector('interactiveImport'), - (tracks, interactiveImport) => { - - const selectedTracksByItem = _.map(interactiveImport.items, (item) => { - return { id: item.id, tracks: _.map(item.tracks, (track) => { - return track.id; - }) }; - }); - - return { - ...tracks, - selectedTracksByItem - }; - } - ); -} - -const mapDispatchToProps = { - fetchTracks, - setTracksSort, - clearTracks, - updateInteractiveImportItem -}; - -class SelectTrackModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - artistId, - albumId, - albumReleaseId - } = this.props; - - this.props.fetchTracks({ artistId, albumId, albumReleaseId }); - } - - componentWillUnmount() { - // This clears the tracks for the queue and hides the queue - // We'll need another place to store tracks for manual import - this.props.clearTracks(); - } - - // - // Listeners - - onSortPress = (sortKey, sortDirection) => { - this.props.setTracksSort({ sortKey, sortDirection }); - } - - onTracksSelect = (trackIds) => { - const tracks = _.reduce(this.props.items, (acc, item) => { - if (trackIds.indexOf(item.id) > -1) { - acc.push(item); - } - - return acc; - }, []); - - this.props.updateInteractiveImportItem({ - id: this.props.id, - tracks: _.sortBy(tracks, 'trackNumber') - }); - - this.props.onModalClose(true); - } - - // - // Render - - render() { - return ( - - ); - } -} - -SelectTrackModalContentConnector.propTypes = { - id: PropTypes.number.isRequired, - artistId: PropTypes.number.isRequired, - albumId: PropTypes.number.isRequired, - albumReleaseId: PropTypes.number.isRequired, - rejections: PropTypes.arrayOf(PropTypes.object).isRequired, - audioTags: PropTypes.object.isRequired, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - fetchTracks: PropTypes.func.isRequired, - setTracksSort: PropTypes.func.isRequired, - clearTracks: PropTypes.func.isRequired, - updateInteractiveImportItem: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(SelectTrackModalContentConnector); diff --git a/frontend/src/InteractiveImport/Track/SelectTrackRow.js b/frontend/src/InteractiveImport/Track/SelectTrackRow.js deleted file mode 100644 index f7dea7af3..000000000 --- a/frontend/src/InteractiveImport/Track/SelectTrackRow.js +++ /dev/null @@ -1,121 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import TableRowButton from 'Components/Table/TableRowButton'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import Icon from 'Components/Icon'; -import Popover from 'Components/Tooltip/Popover'; - -class SelectTrackRow extends Component { - - // - // Listeners - - onPress = () => { - const { - id, - isSelected - } = this.props; - - this.props.onSelectedChange({ id, value: !isSelected }); - } - - // - // Render - - render() { - const { - id, - mediumNumber, - trackNumber, - title, - hasFile, - importSelected, - isSelected, - isDisabled, - onSelectedChange - } = this.props; - - let iconName = icons.UNKNOWN; - let iconKind = kinds.DEFAULT; - let iconTip = ''; - - if (hasFile && !importSelected) { - iconName = icons.DOWNLOADED; - iconKind = kinds.DEFAULT; - iconTip = 'Track already in library.'; - } else if (!hasFile && !importSelected) { - iconName = icons.UNKNOWN; - iconKind = kinds.DEFAULT; - iconTip = 'Track missing from library and no import selected.'; - } else if (importSelected && hasFile) { - iconName = icons.FILEIMPORT; - iconKind = kinds.WARNING; - iconTip = 'Warning: Existing track will be replaced by download.'; - } else if (importSelected && !hasFile) { - iconName = icons.FILEIMPORT; - iconKind = kinds.DEFAULT; - iconTip = 'Track missing from library and selected for import.'; - } - - // isDisabled can only be true if importSelected is true - if (isDisabled) { - iconTip = `${iconTip}\nAnother file is selected to import for this track.`; - } - - return ( - - - - - {mediumNumber} - - - - {trackNumber} - - - - {title} - - - - - } - title={'Track status'} - body={iconTip} - position={tooltipPositions.LEFT} - /> - - - ); - } -} - -SelectTrackRow.propTypes = { - id: PropTypes.number.isRequired, - mediumNumber: PropTypes.number.isRequired, - trackNumber: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - hasFile: PropTypes.bool.isRequired, - importSelected: PropTypes.bool.isRequired, - isSelected: PropTypes.bool, - isDisabled: PropTypes.bool, - onSelectedChange: PropTypes.func.isRequired -}; - -export default SelectTrackRow; diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.js b/frontend/src/InteractiveSearch/InteractiveSearch.js index 1581f2b69..afd5b4f53 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.js +++ b/frontend/src/InteractiveSearch/InteractiveSearch.js @@ -1,13 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { align, icons, sortDirections } from 'Helpers/Props'; +import { icons, sortDirections } from 'Helpers/Props'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import Icon from 'Components/Icon'; -import FilterMenu from 'Components/Menu/FilterMenu'; -import PageMenuButton from 'Components/Menu/PageMenuButton'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; import InteractiveSearchRow from './InteractiveSearchRow'; import styles from './InteractiveSearch.css'; @@ -90,34 +87,16 @@ function InteractiveSearch(props) { error, totalReleasesCount, items, - selectedFilterKey, - filters, - customFilters, sortKey, sortDirection, - type, longDateFormat, timeFormat, onSortPress, - onFilterSelect, onGrabPress } = props; return (
-
- -
- { isFetching ? : null } @@ -192,16 +171,12 @@ InteractiveSearch.propTypes = { error: PropTypes.object, totalReleasesCount: PropTypes.number.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, sortDirection: PropTypes.string, type: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired, onSortPress: PropTypes.func.isRequired, - onFilterSelect: PropTypes.func.isRequired, onGrabPress: PropTypes.func.isRequired }; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchFilterMenu.js b/frontend/src/InteractiveSearch/InteractiveSearchFilterMenu.js new file mode 100644 index 000000000..32b776391 --- /dev/null +++ b/frontend/src/InteractiveSearch/InteractiveSearchFilterMenu.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align } from 'Helpers/Props'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import PageMenuButton from 'Components/Menu/PageMenuButton'; +import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; +import styles from './InteractiveSearch.css'; + +function InteractiveSearchFilterMenu(props) { + const { + selectedFilterKey, + filters, + customFilters, + onFilterSelect + } = props; + + return ( +
+ +
+ ); +} + +InteractiveSearchFilterMenu.propTypes = { + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + onFilterSelect: PropTypes.func.isRequired +}; + +export default InteractiveSearchFilterMenu; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchFilterMenuConnector.js b/frontend/src/InteractiveSearch/InteractiveSearchFilterMenuConnector.js new file mode 100644 index 000000000..a563123db --- /dev/null +++ b/frontend/src/InteractiveSearch/InteractiveSearchFilterMenuConnector.js @@ -0,0 +1,49 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as releaseActions from 'Store/Actions/releaseActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import InteractiveSearchFilterMenu from './InteractiveSearchFilterMenu'; + +function createMapStateToProps(appState, { type }) { + return createSelector( + createClientSideCollectionSelector('releases', `releases.${type}`), + (releases) => { + return { + ...releases + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onFilterSelect(selectedFilterKey) { + const action = props.type === 'album' ? + releaseActions.setAlbumReleasesFilter : + releaseActions.setArtistReleasesFilter; + dispatch(action({ selectedFilterKey })); + } + }; +} + +class InteractiveSearchFilterMenuConnector extends Component { + + // + // Render + + render() { + const { + ...otherProps + } = this.props; + + return ( + + + ); + } +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchFilterMenuConnector); diff --git a/frontend/src/InteractiveSearch/InteractiveSearchTable.js b/frontend/src/InteractiveSearch/InteractiveSearchTable.js new file mode 100644 index 000000000..34e293362 --- /dev/null +++ b/frontend/src/InteractiveSearch/InteractiveSearchTable.js @@ -0,0 +1,23 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import InteractiveSearchConnector from './InteractiveSearchConnector'; + +function InteractiveSearchTable(props) { + const { + type, + ...otherProps + } = props; + + return ( + + ); +} + +InteractiveSearchTable.propTypes = { + type: PropTypes.string.isRequired +}; + +export default InteractiveSearchTable; diff --git a/frontend/src/Organize/OrganizePreviewModalContentConnector.js b/frontend/src/Organize/OrganizePreviewModalContentConnector.js index deec48a13..3e06ded42 100644 --- a/frontend/src/Organize/OrganizePreviewModalContentConnector.js +++ b/frontend/src/Organize/OrganizePreviewModalContentConnector.js @@ -40,13 +40,13 @@ class OrganizePreviewModalContentConnector extends Component { componentDidMount() { const { - artistId, - albumId + authorId, + bookId } = this.props; this.props.fetchOrganizePreview({ - artistId, - albumId + authorId, + bookId }); this.props.fetchNamingSettings(); @@ -58,7 +58,7 @@ class OrganizePreviewModalContentConnector extends Component { onOrganizePress = (files) => { this.props.executeCommand({ name: commandNames.RENAME_FILES, - artistId: this.props.artistId, + authorId: this.props.authorId, files }); @@ -79,8 +79,8 @@ class OrganizePreviewModalContentConnector extends Component { } OrganizePreviewModalContentConnector.propTypes = { - artistId: PropTypes.number.isRequired, - albumId: PropTypes.number, + authorId: PropTypes.number.isRequired, + bookId: PropTypes.number, fetchOrganizePreview: PropTypes.func.isRequired, fetchNamingSettings: PropTypes.func.isRequired, executeCommand: PropTypes.func.isRequired, diff --git a/frontend/src/Retag/RetagPreviewModalContentConnector.js b/frontend/src/Retag/RetagPreviewModalContentConnector.js index ce3a64776..ea40a63f4 100644 --- a/frontend/src/Retag/RetagPreviewModalContentConnector.js +++ b/frontend/src/Retag/RetagPreviewModalContentConnector.js @@ -36,13 +36,13 @@ class RetagPreviewModalContentConnector extends Component { componentDidMount() { const { - artistId, - albumId + authorId, + bookId } = this.props; this.props.fetchRetagPreview({ - artistId, - albumId + authorId, + bookId }); } @@ -52,7 +52,7 @@ class RetagPreviewModalContentConnector extends Component { onRetagPress = (files) => { this.props.executeCommand({ name: commandNames.RETAG_FILES, - artistId: this.props.artistId, + authorId: this.props.authorId, files }); @@ -73,8 +73,8 @@ class RetagPreviewModalContentConnector extends Component { } RetagPreviewModalContentConnector.propTypes = { - artistId: PropTypes.number.isRequired, - albumId: PropTypes.number, + authorId: PropTypes.number.isRequired, + bookId: PropTypes.number, isPopulated: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired, fetchRetagPreview: PropTypes.func.isRequired, diff --git a/frontend/src/Search/AddNewItem.js b/frontend/src/Search/AddNewItem.js index 46008b6a3..12dbb7d6f 100644 --- a/frontend/src/Search/AddNewItem.js +++ b/frontend/src/Search/AddNewItem.js @@ -100,7 +100,7 @@ class AddNewItem extends Component { className={styles.searchInput} name="searchBox" value={term} - placeholder="eg. Breaking Benjamin, readarr:854a1807-025b-42a8-ba8c-2a39717f1d25" + placeholder="eg. War and Peace, goodreads:656, isbn:067003469X, asin:B00JCDK5ME" autoFocus={true} onChange={this.onSearchInputChange} /> @@ -162,8 +162,8 @@ class AddNewItem extends Component {
Couldn't find any results for '{term}'
You can also search using the - MusicBrainz ID - of an artist e.g. readarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234 + Goodreads ID + of a book (e.g. goodreads:656), the isbn (e.g. isbn:067003469X) or the asin (e.g. asin:B00JCDK5ME)
} @@ -171,11 +171,11 @@ class AddNewItem extends Component { { !term &&
-
It's easy to add a new artist, just start typing the name of the artist you want to add.
+
It's easy to add a new author or book, just start typing the name of the item you want to add.
You can also search using the - MusicBrainz ID - of an artist e.g. readarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234 + Goodreads ID + of a book (e.g. goodreads:656), the isbn (e.g. isbn:067003469X) or the asin (e.g. asin:B00JCDK5ME)
} diff --git a/frontend/src/Search/Album/AddNewAlbumModalContent.js b/frontend/src/Search/Album/AddNewAlbumModalContent.js index d0a91c12a..00f439389 100644 --- a/frontend/src/Search/Album/AddNewAlbumModalContent.js +++ b/frontend/src/Search/Album/AddNewAlbumModalContent.js @@ -114,7 +114,7 @@ class AddNewAlbumModalContent extends Component {
} @@ -225,21 +171,6 @@ class Naming extends Component { /> - - Album Folder Format - - ?} - onChange={onInputChange} - {...settings.albumFolderFormat} - helpTexts={albumFolderFormatHelpTexts} - errors={[...albumFolderFormatErrors, ...settings.albumFolderFormat.errors]} - /> - - { namingModalOptions && + + Calibre Library + + + + + { + isCalibreLibrary !== undefined && isCalibreLibrary.value && +
+ + Calibre Host + + + + + + Calibre Port + + + + + + Calibre Url Base + + + + + + Calibre Username + + + + + + Calibre Password + + + + + + Convert to format + + + + + + Calibre Output Profile + + + + + + Use SSL + + + +
+ } + Monitor @@ -112,7 +237,7 @@ function EditRootFolderModalContent(props) { name="defaultMonitorOption" onChange={onInputChange} {...defaultMonitorOption} - helpText="Default Monitoring Options for albums by artists detected in this folder" + helpText="Default Monitoring Options for books by authors detected in this folder" /> @@ -123,7 +248,7 @@ function EditRootFolderModalContent(props) { @@ -148,7 +273,7 @@ function EditRootFolderModalContent(props) { diff --git a/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContent.js b/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContent.js index 672e2f28c..1e6ec83de 100644 --- a/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContent.js @@ -12,9 +12,6 @@ import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormLabel from 'Components/Form/FormLabel'; import FormInputGroup from 'Components/Form/FormInputGroup'; -import PrimaryTypeItems from './PrimaryTypeItems'; -import SecondaryTypeItems from './SecondaryTypeItems'; -import ReleaseStatusItems from './ReleaseStatusItems'; import styles from './EditMetadataProfileModalContent.css'; function EditMetadataProfileModalContent(props) { @@ -23,8 +20,6 @@ function EditMetadataProfileModalContent(props) { error, isSaving, saveError, - primaryAlbumTypes, - secondaryAlbumTypes, item, isInUse, onInputChange, @@ -37,9 +32,13 @@ function EditMetadataProfileModalContent(props) { const { id, name, - primaryAlbumTypes: itemPrimaryAlbumTypes, - secondaryAlbumTypes: itemSecondaryAlbumTypes, - releaseStatuses: itemReleaseStatuses + minRating, + minRatingCount, + skipMissingDate, + skipMissingIsbn, + skipPartsAndSets, + skipSeriesSecondary, + allowedLanguages } = item; return ( @@ -73,29 +72,86 @@ function EditMetadataProfileModalContent(props) { /> - - - - - + + Minimum Rating + + + + + + Minimum Number of Ratings + + + + + + Skip books with missing release date + + + + + + Skip books with no ISBN or ASIN + + + + + + Skip part books and sets + + + + + + Skip secondary series books + + + + + + Allowed Languages + + + } @@ -140,9 +196,6 @@ EditMetadataProfileModalContent.propTypes = { error: PropTypes.object, isSaving: PropTypes.bool.isRequired, saveError: PropTypes.object, - primaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired, - secondaryAlbumTypes: PropTypes.arrayOf(PropTypes.object).isRequired, - releaseStatuses: PropTypes.arrayOf(PropTypes.object).isRequired, item: PropTypes.object.isRequired, isInUse: PropTypes.bool.isRequired, onInputChange: PropTypes.func.isRequired, diff --git a/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContentConnector.js index 6fd45d3c9..1bf4b8767 100644 --- a/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContentConnector.js +++ b/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContentConnector.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -8,87 +7,12 @@ import createProviderSettingsSelector from 'Store/Selectors/createProviderSettin import { fetchMetadataProfileSchema, setMetadataProfileValue, saveMetadataProfile } from 'Store/Actions/settingsActions'; import EditMetadataProfileModalContent from './EditMetadataProfileModalContent'; -function createPrimaryAlbumTypesSelector() { - return createSelector( - createProviderSettingsSelector('metadataProfiles'), - (metadataProfile) => { - const primaryAlbumTypes = metadataProfile.item.primaryAlbumTypes; - if (!primaryAlbumTypes || !primaryAlbumTypes.value) { - return []; - } - - return _.reduceRight(primaryAlbumTypes.value, (result, { allowed, albumType }) => { - if (allowed) { - result.push({ - key: albumType.id, - value: albumType.name - }); - } - - return result; - }, []); - } - ); -} - -function createSecondaryAlbumTypesSelector() { - return createSelector( - createProviderSettingsSelector('metadataProfiles'), - (metadataProfile) => { - const secondaryAlbumTypes = metadataProfile.item.secondaryAlbumTypes; - if (!secondaryAlbumTypes || !secondaryAlbumTypes.value) { - return []; - } - - return _.reduceRight(secondaryAlbumTypes.value, (result, { allowed, albumType }) => { - if (allowed) { - result.push({ - key: albumType.id, - value: albumType.name - }); - } - - return result; - }, []); - } - ); -} - -function createReleaseStatusesSelector() { - return createSelector( - createProviderSettingsSelector('metadataProfiles'), - (metadataProfile) => { - const releaseStatuses = metadataProfile.item.releaseStatuses; - if (!releaseStatuses || !releaseStatuses.value) { - return []; - } - - return _.reduceRight(releaseStatuses.value, (result, { allowed, releaseStatus }) => { - if (allowed) { - result.push({ - key: releaseStatus.id, - value: releaseStatus.name - }); - } - - return result; - }, []); - } - ); -} - function createMapStateToProps() { return createSelector( createProviderSettingsSelector('metadataProfiles'), - createPrimaryAlbumTypesSelector(), - createSecondaryAlbumTypesSelector(), - createReleaseStatusesSelector(), createProfileInUseSelector('metadataProfileId'), - (metadataProfile, primaryAlbumTypes, secondaryAlbumTypes, releaseStatuses, isInUse) => { + (metadataProfile, isInUse) => { return { - primaryAlbumTypes, - secondaryAlbumTypes, - releaseStatuses, ...metadataProfile, isInUse }; @@ -139,59 +63,16 @@ class EditMetadataProfileModalContentConnector extends Component { this.props.saveMetadataProfile({ id: this.props.id }); } - onMetadataPrimaryTypeItemAllowedChange = (id, allowed) => { - const metadataProfile = _.cloneDeep(this.props.item); - - const item = _.find(metadataProfile.primaryAlbumTypes.value, (i) => i.albumType.id === id); - item.allowed = allowed; - - this.props.setMetadataProfileValue({ - name: 'primaryAlbumTypes', - value: metadataProfile.primaryAlbumTypes.value - }); - } - - onMetadataSecondaryTypeItemAllowedChange = (id, allowed) => { - const metadataProfile = _.cloneDeep(this.props.item); - - const item = _.find(metadataProfile.secondaryAlbumTypes.value, (i) => i.albumType.id === id); - item.allowed = allowed; - - this.props.setMetadataProfileValue({ - name: 'secondaryAlbumTypes', - value: metadataProfile.secondaryAlbumTypes.value - }); - } - - onMetadataReleaseStatusItemAllowedChange = (id, allowed) => { - const metadataProfile = _.cloneDeep(this.props.item); - - const item = _.find(metadataProfile.releaseStatuses.value, (i) => i.releaseStatus.id === id); - item.allowed = allowed; - - this.props.setMetadataProfileValue({ - name: 'releaseStatuses', - value: metadataProfile.releaseStatuses.value - }); - } - // // Render render() { - if (_.isEmpty(this.props.item.primaryAlbumTypes) && !this.props.isFetching) { - return null; - } - return ( ); } diff --git a/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js b/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js index 5943de616..986b9e8d9 100644 --- a/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js +++ b/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { icons, kinds } from 'Helpers/Props'; import Card from 'Components/Card'; -import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import EditMetadataProfileModalConnector from './EditMetadataProfileModalConnector'; @@ -64,8 +63,6 @@ class MetadataProfile extends Component { const { id, name, - primaryAlbumTypes, - secondaryAlbumTypes, isDeleting } = this.props; @@ -88,46 +85,6 @@ class MetadataProfile extends Component { />
-
- { - primaryAlbumTypes.map((item) => { - if (!item.allowed) { - return null; - } - - return ( - - ); - }) - } -
- -
- { - secondaryAlbumTypes.map((item) => { - if (!item.allowed) { - return null; - } - - return ( - - ); - }) - } -
- { - const { - albumTypeId, - onMetadataPrimaryTypeItemAllowedChange - } = this.props; - - onMetadataPrimaryTypeItemAllowedChange(albumTypeId, value); - } - - // - // Render - - render() { - const { - name, - allowed - } = this.props; - - return ( -
- -
- ); - } -} - -PrimaryTypeItem.propTypes = { - albumTypeId: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - allowed: PropTypes.bool.isRequired, - sortIndex: PropTypes.number.isRequired, - onMetadataPrimaryTypeItemAllowedChange: PropTypes.func -}; - -export default PrimaryTypeItem; diff --git a/frontend/src/Settings/Profiles/Metadata/PrimaryTypeItems.js b/frontend/src/Settings/Profiles/Metadata/PrimaryTypeItems.js deleted file mode 100644 index 487adbbd6..000000000 --- a/frontend/src/Settings/Profiles/Metadata/PrimaryTypeItems.js +++ /dev/null @@ -1,87 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FormGroup from 'Components/Form/FormGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import FormInputHelpText from 'Components/Form/FormInputHelpText'; -import PrimaryTypeItem from './PrimaryTypeItem'; -import styles from './TypeItems.css'; - -class PrimaryTypeItems extends Component { - - // - // Render - - render() { - const { - metadataProfileItems, - errors, - warnings, - ...otherProps - } = this.props; - - return ( - - Primary Types -
- - { - errors.map((error, index) => { - return ( - - ); - }) - } - - { - warnings.map((warning, index) => { - return ( - - ); - }) - } - -
- { - metadataProfileItems.map(({ allowed, albumType }, index) => { - return ( - - ); - }).reverse() - } -
-
-
- ); - } -} - -PrimaryTypeItems.propTypes = { - metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired, - errors: PropTypes.arrayOf(PropTypes.object), - warnings: PropTypes.arrayOf(PropTypes.object), - formLabel: PropTypes.string -}; - -PrimaryTypeItems.defaultProps = { - errors: [], - warnings: [] -}; - -export default PrimaryTypeItems; diff --git a/frontend/src/Settings/Profiles/Metadata/ReleaseStatusItem.js b/frontend/src/Settings/Profiles/Metadata/ReleaseStatusItem.js deleted file mode 100644 index 71fe7f76c..000000000 --- a/frontend/src/Settings/Profiles/Metadata/ReleaseStatusItem.js +++ /dev/null @@ -1,60 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import classNames from 'classnames'; -import CheckInput from 'Components/Form/CheckInput'; -import styles from './TypeItem.css'; - -class ReleaseStatusItem extends Component { - - // - // Listeners - - onAllowedChange = ({ value }) => { - const { - albumTypeId, - onMetadataReleaseStatusItemAllowedChange - } = this.props; - - onMetadataReleaseStatusItemAllowedChange(albumTypeId, value); - } - - // - // Render - - render() { - const { - name, - allowed - } = this.props; - - return ( -
- -
- ); - } -} - -ReleaseStatusItem.propTypes = { - albumTypeId: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - allowed: PropTypes.bool.isRequired, - sortIndex: PropTypes.number.isRequired, - onMetadataReleaseStatusItemAllowedChange: PropTypes.func -}; - -export default ReleaseStatusItem; diff --git a/frontend/src/Settings/Profiles/Metadata/ReleaseStatusItems.js b/frontend/src/Settings/Profiles/Metadata/ReleaseStatusItems.js deleted file mode 100644 index 31a24dff3..000000000 --- a/frontend/src/Settings/Profiles/Metadata/ReleaseStatusItems.js +++ /dev/null @@ -1,87 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FormGroup from 'Components/Form/FormGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import FormInputHelpText from 'Components/Form/FormInputHelpText'; -import ReleaseStatusItem from './ReleaseStatusItem'; -import styles from './TypeItems.css'; - -class ReleaseStatusItems extends Component { - - // - // Render - - render() { - const { - metadataProfileItems, - errors, - warnings, - ...otherProps - } = this.props; - - return ( - - Release Statuses -
- - { - errors.map((error, index) => { - return ( - - ); - }) - } - - { - warnings.map((warning, index) => { - return ( - - ); - }) - } - -
- { - metadataProfileItems.map(({ allowed, releaseStatus }, index) => { - return ( - - ); - }) - } -
-
-
- ); - } -} - -ReleaseStatusItems.propTypes = { - metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired, - errors: PropTypes.arrayOf(PropTypes.object), - warnings: PropTypes.arrayOf(PropTypes.object), - formLabel: PropTypes.string -}; - -ReleaseStatusItems.defaultProps = { - errors: [], - warnings: [] -}; - -export default ReleaseStatusItems; diff --git a/frontend/src/Settings/Profiles/Metadata/SecondaryTypeItem.js b/frontend/src/Settings/Profiles/Metadata/SecondaryTypeItem.js deleted file mode 100644 index 79995a920..000000000 --- a/frontend/src/Settings/Profiles/Metadata/SecondaryTypeItem.js +++ /dev/null @@ -1,60 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import classNames from 'classnames'; -import CheckInput from 'Components/Form/CheckInput'; -import styles from './TypeItem.css'; - -class SecondaryTypeItem extends Component { - - // - // Listeners - - onAllowedChange = ({ value }) => { - const { - albumTypeId, - onMetadataSecondaryTypeItemAllowedChange - } = this.props; - - onMetadataSecondaryTypeItemAllowedChange(albumTypeId, value); - } - - // - // Render - - render() { - const { - name, - allowed - } = this.props; - - return ( -
- -
- ); - } -} - -SecondaryTypeItem.propTypes = { - albumTypeId: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - allowed: PropTypes.bool.isRequired, - sortIndex: PropTypes.number.isRequired, - onMetadataSecondaryTypeItemAllowedChange: PropTypes.func -}; - -export default SecondaryTypeItem; diff --git a/frontend/src/Settings/Profiles/Metadata/SecondaryTypeItems.js b/frontend/src/Settings/Profiles/Metadata/SecondaryTypeItems.js deleted file mode 100644 index 3f46d710a..000000000 --- a/frontend/src/Settings/Profiles/Metadata/SecondaryTypeItems.js +++ /dev/null @@ -1,87 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FormGroup from 'Components/Form/FormGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import FormInputHelpText from 'Components/Form/FormInputHelpText'; -import SecondaryTypeItem from './SecondaryTypeItem'; -import styles from './TypeItems.css'; - -class SecondaryTypeItems extends Component { - - // - // Render - - render() { - const { - metadataProfileItems, - errors, - warnings, - ...otherProps - } = this.props; - - return ( - - Secondary Types -
- - { - errors.map((error, index) => { - return ( - - ); - }) - } - - { - warnings.map((warning, index) => { - return ( - - ); - }) - } - -
- { - metadataProfileItems.map(({ allowed, albumType }, index) => { - return ( - - ); - }) - } -
-
-
- ); - } -} - -SecondaryTypeItems.propTypes = { - metadataProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired, - errors: PropTypes.arrayOf(PropTypes.object), - warnings: PropTypes.arrayOf(PropTypes.object), - formLabel: PropTypes.string -}; - -SecondaryTypeItems.defaultProps = { - errors: [], - warnings: [] -}; - -export default SecondaryTypeItems; diff --git a/frontend/src/Settings/Profiles/Metadata/TypeItem.css b/frontend/src/Settings/Profiles/Metadata/TypeItem.css deleted file mode 100644 index 908f3bde6..000000000 --- a/frontend/src/Settings/Profiles/Metadata/TypeItem.css +++ /dev/null @@ -1,25 +0,0 @@ -.metadataProfileItem { - display: flex; - align-items: stretch; - width: 100%; -} - -.checkContainer { - position: relative; - margin-right: 4px; - margin-bottom: 7px; - margin-left: 8px; -} - -.albumTypeName { - display: flex; - flex-grow: 1; - margin-bottom: 0; - margin-left: 2px; - font-weight: normal; - line-height: 36px; -} - -.isDragging { - opacity: 0.25; -} diff --git a/frontend/src/Settings/Profiles/Metadata/TypeItems.css b/frontend/src/Settings/Profiles/Metadata/TypeItems.css deleted file mode 100644 index 3bce22799..000000000 --- a/frontend/src/Settings/Profiles/Metadata/TypeItems.css +++ /dev/null @@ -1,6 +0,0 @@ -.albumTypes { - margin-top: 10px; - /* TODO: This should consider the number of types in the list */ - min-height: 200px; - user-select: none; -} diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.js b/frontend/src/Settings/Quality/Definition/QualityDefinition.js index 9c5258019..3b222e22d 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinition.js +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.js @@ -46,28 +46,12 @@ class QualityDefinition extends Component { constructor(props, context) { super(props, context); - this._forceUpdateTimeout = null; - this.state = { sliderMinSize: getSliderValue(props.minSize, slider.min), sliderMaxSize: getSliderValue(props.maxSize, slider.max) }; } - componentDidMount() { - // A hack to deal with a bug in the slider component until a fix for it - // lands and an updated version is available. - // See: https://github.com/mpowaga/react-slider/issues/115 - - this._forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 1); - } - - componentWillUnmount() { - if (this._forceUpdateTimeout) { - clearTimeout(this._forceUpdateTimeout); - } - } - // // Listeners @@ -167,11 +151,11 @@ class QualityDefinition extends Component { step={slider.step} minDistance={10} value={[sliderMinSize, sliderMaxSize]} - withBars={true} + withTracks={true} snapDragDisabled={true} className={styles.slider} - barClassName={styles.bar} - handleClassName={styles.handle} + trackClassName={styles.bar} + thumbClassName={styles.handle} onChange={this.onSliderChange} onAfterChange={this.onAfterSliderChange} /> diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js index 18a3fb435..0f8f47f95 100644 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js @@ -11,7 +11,7 @@ function findMatchingItems(ids, items) { function createMatchingArtistSelector() { return createSelector( - (state, { artistIds }) => artistIds, + (state, { authorIds }) => authorIds, createAllArtistSelector(), findMatchingItems ); diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js index b2aceb47e..731586e31 100644 --- a/frontend/src/Settings/Tags/Tag.js +++ b/frontend/src/Settings/Tags/Tag.js @@ -56,7 +56,7 @@ class Tag extends Component { importListIds, notificationIds, restrictionIds, - artistIds + authorIds } = this.props; const { @@ -69,7 +69,7 @@ class Tag extends Component { importListIds.length || notificationIds.length || restrictionIds.length || - artistIds.length + authorIds.length ); return ( @@ -86,9 +86,9 @@ class Tag extends Component { isTagUsed &&
{ - !!artistIds.length && + !!authorIds.length &&
- {artistIds.length} artists + {authorIds.length} artists
} @@ -132,7 +132,7 @@ class Tag extends Component { { - dispatch(updateAlbums(section, state.items, albumIds, { + dispatch(updateAlbums(section, state.items, bookIds, { isSaving: false, monitored })); @@ -32,7 +32,7 @@ function createBatchToggleAlbumMonitoredHandler(section, fetchHandler) { }); promise.fail(() => { - dispatch(updateAlbums(section, state.items, albumIds, { + dispatch(updateAlbums(section, state.items, bookIds, { isSaving: false })); }); diff --git a/frontend/src/Store/Actions/Settings/rootFolders.js b/frontend/src/Store/Actions/Settings/rootFolders.js index d7bcf34e4..461f0ff95 100644 --- a/frontend/src/Store/Actions/Settings/rootFolders.js +++ b/frontend/src/Store/Actions/Settings/rootFolders.js @@ -46,6 +46,11 @@ export default { isPopulated: false, error: null, schema: { + isCalibreLibrary: false, + host: 'localhost', + port: 8080, + useSsl: false, + outputProfile: 0, defaultTags: [] }, isSaving: false, diff --git a/frontend/src/Store/Actions/albumActions.js b/frontend/src/Store/Actions/albumActions.js index 228d64d69..2e6508765 100644 --- a/frontend/src/Store/Actions/albumActions.js +++ b/frontend/src/Store/Actions/albumActions.js @@ -57,28 +57,11 @@ export const defaultState = { isSortable: true, isVisible: true }, - { - name: 'secondaryTypes', - label: 'Secondary Types', - isSortable: true, - isVisible: false - }, - { - name: 'mediumCount', - label: 'Media Count', - isVisible: false - }, { name: 'trackCount', label: 'Track Count', isVisible: false }, - { - name: 'duration', - label: 'Duration', - isSortable: true, - isVisible: false - }, { name: 'rating', label: 'Rating', @@ -157,7 +140,7 @@ export const actionHandlers = handleThunks({ [TOGGLE_ALBUM_MONITORED]: function(getState, payload, dispatch) { const { - albumId, + bookId, albumEntity = albumEntities.ALBUMS, monitored } = payload; @@ -165,13 +148,13 @@ export const actionHandlers = handleThunks({ const albumSection = _.last(albumEntity.split('.')); dispatch(updateItem({ - id: albumId, + id: bookId, section: albumSection, isSaving: true })); const promise = createAjaxRequest({ - url: `/album/${albumId}`, + url: `/album/${bookId}`, method: 'PUT', data: JSON.stringify({ monitored }), dataType: 'json' @@ -179,7 +162,7 @@ export const actionHandlers = handleThunks({ promise.done((data) => { dispatch(updateItem({ - id: albumId, + id: bookId, section: albumSection, isSaving: false, monitored @@ -188,7 +171,7 @@ export const actionHandlers = handleThunks({ promise.fail((xhr) => { dispatch(updateItem({ - id: albumId, + id: bookId, section: albumSection, isSaving: false })); @@ -197,15 +180,15 @@ export const actionHandlers = handleThunks({ [TOGGLE_ALBUMS_MONITORED]: function(getState, payload, dispatch) { const { - albumIds, + bookIds, albumEntity = albumEntities.ALBUMS, monitored } = payload; dispatch(batchActions( - albumIds.map((albumId) => { + bookIds.map((bookId) => { return updateItem({ - id: albumId, + id: bookId, section: albumEntity, isSaving: true }); @@ -215,15 +198,15 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/album/monitor', method: 'PUT', - data: JSON.stringify({ albumIds, monitored }), + data: JSON.stringify({ bookIds, monitored }), dataType: 'json' }).request; promise.done((data) => { dispatch(batchActions( - albumIds.map((albumId) => { + bookIds.map((bookId) => { return updateItem({ - id: albumId, + id: bookId, section: albumEntity, isSaving: false, monitored @@ -234,9 +217,9 @@ export const actionHandlers = handleThunks({ promise.fail((xhr) => { dispatch(batchActions( - albumIds.map((albumId) => { + bookIds.map((bookId) => { return updateItem({ - id: albumId, + id: bookId, section: albumEntity, isSaving: false }); diff --git a/frontend/src/Store/Actions/albumHistoryActions.js b/frontend/src/Store/Actions/albumHistoryActions.js index a0c832784..2bf6df39b 100644 --- a/frontend/src/Store/Actions/albumHistoryActions.js +++ b/frontend/src/Store/Actions/albumHistoryActions.js @@ -48,7 +48,7 @@ export const actionHandlers = handleThunks({ page: 1, sortKey: 'date', sortDirection: sortDirections.DESCENDING, - albumId: payload.albumId + bookId: payload.bookId }; const promise = createAjaxRequest({ @@ -82,7 +82,7 @@ export const actionHandlers = handleThunks({ [ALBUM_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) { const { historyId, - albumId + bookId } = payload; const promise = createAjaxRequest({ @@ -94,7 +94,7 @@ export const actionHandlers = handleThunks({ }).request; promise.done(() => { - dispatch(fetchAlbumHistory({ albumId })); + dispatch(fetchAlbumHistory({ bookId })); }); } }); diff --git a/frontend/src/Store/Actions/albumStudioActions.js b/frontend/src/Store/Actions/albumStudioActions.js index 5225c27cf..dbbc93fd3 100644 --- a/frontend/src/Store/Actions/albumStudioActions.js +++ b/frontend/src/Store/Actions/albumStudioActions.js @@ -100,14 +100,14 @@ export const actionHandlers = handleThunks({ [SAVE_ALBUM_STUDIO]: function(getState, payload, dispatch) { const { - artistIds, + authorIds, monitored, monitor } = payload; const artist = []; - artistIds.forEach((id) => { + authorIds.forEach((id) => { const artistToUpdate = { id }; if (payload.hasOwnProperty('monitored')) { diff --git a/frontend/src/Store/Actions/artistActions.js b/frontend/src/Store/Actions/artistActions.js index a47dfe272..58a5e584f 100644 --- a/frontend/src/Store/Actions/artistActions.js +++ b/frontend/src/Store/Actions/artistActions.js @@ -224,7 +224,7 @@ export const actionHandlers = handleThunks({ [TOGGLE_ARTIST_MONITORED]: (getState, payload, dispatch) => { const { - artistId: id, + authorId: id, monitored } = payload; @@ -266,7 +266,7 @@ export const actionHandlers = handleThunks({ [TOGGLE_ALBUM_MONITORED]: function(getState, payload, dispatch) { const { - artistId: id, + authorId: id, seasonNumber, monitored } = payload; @@ -296,7 +296,7 @@ export const actionHandlers = handleThunks({ }).request; promise.done((data) => { - const albums = _.filter(getState().albums.items, { artistId: id, seasonNumber }); + const albums = _.filter(getState().albums.items, { authorId: id, seasonNumber }); dispatch(batchActions([ updateItem({ diff --git a/frontend/src/Store/Actions/artistEditorActions.js b/frontend/src/Store/Actions/artistEditorActions.js index 238419df0..30652c50c 100644 --- a/frontend/src/Store/Actions/artistEditorActions.js +++ b/frontend/src/Store/Actions/artistEditorActions.js @@ -110,7 +110,7 @@ export const actionHandlers = handleThunks({ })); const promise = createAjaxRequest({ - url: '/artist/editor', + url: '/author/editor', method: 'PUT', data: JSON.stringify(payload), dataType: 'json' @@ -150,7 +150,7 @@ export const actionHandlers = handleThunks({ })); const promise = createAjaxRequest({ - url: '/artist/editor', + url: '/author/editor', method: 'DELETE', data: JSON.stringify(payload), dataType: 'json' diff --git a/frontend/src/Store/Actions/artistHistoryActions.js b/frontend/src/Store/Actions/artistHistoryActions.js index 237004ae3..92d1966df 100644 --- a/frontend/src/Store/Actions/artistHistoryActions.js +++ b/frontend/src/Store/Actions/artistHistoryActions.js @@ -73,8 +73,8 @@ export const actionHandlers = handleThunks({ [ARTIST_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) { const { historyId, - artistId, - albumId + authorId, + bookId } = payload; const promise = createAjaxRequest({ @@ -86,7 +86,7 @@ export const actionHandlers = handleThunks({ }).request; promise.done(() => { - dispatch(fetchArtistHistory({ artistId, albumId })); + dispatch(fetchArtistHistory({ authorId, bookId })); }); } }); diff --git a/frontend/src/Store/Actions/blacklistActions.js b/frontend/src/Store/Actions/blacklistActions.js index 2dde21b1e..d7d83d229 100644 --- a/frontend/src/Store/Actions/blacklistActions.js +++ b/frontend/src/Store/Actions/blacklistActions.js @@ -27,7 +27,7 @@ export const defaultState = { columns: [ { - name: 'artist.sortName', + name: 'authors.sortName', label: 'Artist Name', isSortable: true, isVisible: true diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js index aee74f14f..804cf4105 100644 --- a/frontend/src/Store/Actions/calendarActions.js +++ b/frontend/src/Store/Actions/calendarActions.js @@ -345,11 +345,11 @@ export const actionHandlers = handleThunks({ }, [SEARCH_MISSING]: function(getState, payload, dispatch) { - const { albumIds } = payload; + const { bookIds } = payload; const commandPayload = { name: commandNames.ALBUM_SEARCH, - albumIds + bookIds }; executeCommandHelper(commandPayload, dispatch).then((data) => { diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 8862464e7..1958446f8 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -34,22 +34,17 @@ export const defaultState = { isModifiable: false }, { - name: 'artist.sortName', - label: 'Artist', + name: 'authors.sortName', + label: 'Author', isSortable: true, isVisible: true }, { - name: 'album.title', - label: 'Album Title', + name: 'books.title', + label: 'Book', isSortable: true, isVisible: true }, - { - name: 'trackTitle', - label: 'Track Title', - isVisible: true - }, { name: 'quality', label: 'Quality', diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 183bf9df3..3c37dc5a4 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -21,6 +21,7 @@ import * as artist from './artistActions'; import * as artistEditor from './artistEditorActions'; import * as artistHistory from './artistHistoryActions'; import * as artistIndex from './artistIndexActions'; +import * as series from './seriesActions'; import * as search from './searchActions'; import * as settings from './settingsActions'; import * as system from './systemActions'; @@ -52,6 +53,7 @@ export default [ artistEditor, artistHistory, artistIndex, + series, search, settings, system, diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index 7662a917a..78418deb9 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -55,7 +55,7 @@ export const defaultState = { isFetching: false, isPopulated: false, error: null, - sortKey: 'albumTitle', + sortKey: 'title', sortDirection: sortDirections.ASCENDING, items: [] }, @@ -64,7 +64,7 @@ export const defaultState = { isFetching: false, isPopulated: false, error: null, - sortKey: 'relataivePath', + sortKey: 'relativePath', sortDirection: sortDirections.ASCENDING, items: [] } diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index 1ecc1d978..286d3b977 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -63,20 +63,20 @@ export const defaultState = { isModifiable: false }, { - name: 'artist.sortName', - label: 'Artist', + name: 'authors.sortName', + label: 'Author', isSortable: true, isVisible: true }, { - name: 'album.title', - label: 'Album Title', + name: 'books.title', + label: 'Book Title', isSortable: true, isVisible: true }, { - name: 'album.releaseDate', - label: 'Album Release Date', + name: 'books.releaseDate', + label: 'Release Date', isSortable: true, isVisible: false }, diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index fefb64399..aabd1a91d 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -151,7 +151,7 @@ export const defaultState = { }, artist: { - selectedFilterKey: 'discography-pack' + selectedFilterKey: 'all' } }; diff --git a/frontend/src/Store/Actions/searchActions.js b/frontend/src/Store/Actions/searchActions.js index 806430fc2..fc0dabc42 100644 --- a/frontend/src/Store/Actions/searchActions.js +++ b/frontend/src/Store/Actions/searchActions.js @@ -34,7 +34,6 @@ export const defaultState = { monitor: monitorOptions[0].key, qualityProfileId: 0, metadataProfileId: 0, - albumFolder: true, tags: [] } }; @@ -108,9 +107,9 @@ export const actionHandlers = handleThunks({ [ADD_ARTIST]: function(getState, payload, dispatch) { dispatch(set({ section, isAdding: true })); - const foreignArtistId = payload.foreignArtistId; + const foreignAuthorId = payload.foreignAuthorId; const items = getState().search.items; - const itemToAdd = _.find(items, { foreignId: foreignArtistId }); + const itemToAdd = _.find(items, { foreignId: foreignAuthorId }); const newArtist = getNewArtist(_.cloneDeep(itemToAdd.artist), payload); const promise = createAjaxRequest({ @@ -146,9 +145,9 @@ export const actionHandlers = handleThunks({ [ADD_ALBUM]: function(getState, payload, dispatch) { dispatch(set({ section, isAdding: true })); - const foreignAlbumId = payload.foreignAlbumId; + const foreignBookId = payload.foreignBookId; const items = getState().search.items; - const itemToAdd = _.find(items, { foreignId: foreignAlbumId }); + const itemToAdd = _.find(items, { foreignId: foreignBookId }); const newAlbum = getNewAlbum(_.cloneDeep(itemToAdd.album), payload); const promise = createAjaxRequest({ diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js new file mode 100644 index 000000000..976e624d1 --- /dev/null +++ b/frontend/src/Store/Actions/seriesActions.js @@ -0,0 +1,130 @@ +import { createAction } from 'redux-actions'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; + +// +// Variables + +export const section = 'series'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + sortKey: 'position', + sortDirection: sortDirections.ASCENDING, + items: [], + + columns: [ + { + name: 'monitored', + columnLabel: 'Monitored', + isVisible: true, + isModifiable: false + }, + { + name: 'title', + label: 'Title', + isSortable: true, + isVisible: true + }, + { + name: 'position', + label: 'Number', + isSortable: true, + isVisible: true + }, + { + name: 'releaseDate', + label: 'Release Date', + isSortable: true, + isVisible: true + }, + { + name: 'secondaryTypes', + label: 'Secondary Types', + isSortable: true, + isVisible: false + }, + { + name: 'mediumCount', + label: 'Media Count', + isVisible: false + }, + { + name: 'trackCount', + label: 'Track Count', + isVisible: false + }, + { + name: 'duration', + label: 'Duration', + isSortable: true, + isVisible: false + }, + { + name: 'rating', + label: 'Rating', + isSortable: true, + isVisible: true + }, + { + name: 'status', + label: 'Status', + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] +}; + +// +// Actions Types + +export const FETCH_SERIES = 'series/fetchSeries'; +export const SET_SERIES_SORT = 'albums/setSeriesSort'; +export const CLEAR_SERIES = 'series/clearSeries'; + +// +// Action Creators + +export const fetchSeries = createThunk(FETCH_SERIES); +export const setSeriesSort = createAction(SET_SERIES_SORT); +export const clearSeries = createAction(CLEAR_SERIES); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [FETCH_SERIES]: createFetchHandler(section, '/series') +}); + +// +// Reducers + +export const reducers = createHandleActions({ + + [SET_SERIES_SORT]: createSetClientSideCollectionSortReducer(section), + + [CLEAR_SERIES]: (state) => { + return Object.assign({}, state, { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }); + } + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js index 4df132504..45b264dc5 100644 --- a/frontend/src/Store/Actions/wantedActions.js +++ b/frontend/src/Store/Actions/wantedActions.js @@ -28,20 +28,14 @@ export const defaultState = { columns: [ { - name: 'artist.sortName', - label: 'Artist Name', + name: 'authors.sortName', + label: 'Author', isSortable: true, isVisible: true }, { - name: 'albumTitle', - label: 'Album Title', - isSortable: true, - isVisible: true - }, - { - name: 'albumType', - label: 'Album Type', + name: 'books.title', + label: 'Book', isSortable: true, isVisible: true }, @@ -51,11 +45,6 @@ export const defaultState = { isSortable: true, isVisible: true }, - // { - // name: 'status', - // label: 'Status', - // isVisible: true - // }, { name: 'actions', columnLabel: 'Actions', @@ -102,20 +91,14 @@ export const defaultState = { columns: [ { - name: 'artist.sortName', - label: 'Artist Name', - isSortable: true, - isVisible: true - }, - { - name: 'albumTitle', - label: 'Album Title', + name: 'authors.sortName', + label: 'Author', isSortable: true, isVisible: true }, { - name: 'albumType', - label: 'Album Type', + name: 'books.Title', + label: 'Book', isSortable: true, isVisible: true }, @@ -125,11 +108,6 @@ export const defaultState = { isSortable: true, isVisible: true }, - // { - // name: 'status', - // label: 'Status', - // isVisible: true - // }, { name: 'actions', columnLabel: 'Actions', diff --git a/frontend/src/Store/Middleware/createSentryMiddleware.js b/frontend/src/Store/Middleware/createSentryMiddleware.js index f054e95c9..334d9fd74 100644 --- a/frontend/src/Store/Middleware/createSentryMiddleware.js +++ b/frontend/src/Store/Middleware/createSentryMiddleware.js @@ -80,8 +80,8 @@ export default function createSentryMiddleware() { return; } - const dsn = isProduction ? 'https://c2c8e08845994dbfb7eddb158b408172@sentry.radarr.video/18' : - 'https://c2c8e08845994dbfb7eddb158b408172@sentry.radarr.video/18'; + const dsn = isProduction ? 'https://56c6b0e2fa1041b3b06eaa7abd9850ef@sentry.servarr.com/7' : + 'https://a0ec920735ed4e3e9d27d2cdd9c733bf@sentry.servarr.com/8'; sentry.init({ dsn, diff --git a/frontend/src/Store/Selectors/createAlbumSelector.js b/frontend/src/Store/Selectors/createAlbumSelector.js index 13894a143..56c9843c1 100644 --- a/frontend/src/Store/Selectors/createAlbumSelector.js +++ b/frontend/src/Store/Selectors/createAlbumSelector.js @@ -4,10 +4,10 @@ import albumEntities from 'Album/albumEntities'; function createAlbumSelector() { return createSelector( - (state, { albumId }) => albumId, + (state, { bookId }) => bookId, (state, { albumEntity = albumEntities.ALBUMS }) => _.get(state, albumEntity, { items: [] }), - (albumId, albums) => { - return _.find(albums.items, { id: albumId }); + (bookId, albums) => { + return _.find(albums.items, { id: bookId }); } ); } diff --git a/frontend/src/Store/Selectors/createArtistSelector.js b/frontend/src/Store/Selectors/createArtistSelector.js index 104ef83e3..505be5e10 100644 --- a/frontend/src/Store/Selectors/createArtistSelector.js +++ b/frontend/src/Store/Selectors/createArtistSelector.js @@ -2,11 +2,11 @@ import { createSelector } from 'reselect'; function createArtistSelector() { return createSelector( - (state, { artistId }) => artistId, + (state, { authorId }) => authorId, (state) => state.artist.itemMap, (state) => state.artist.items, - (artistId, itemMap, allArtists) => { - return allArtists[itemMap[artistId]]; + (authorId, itemMap, allArtists) => { + return allArtists[itemMap[authorId]]; } ); } diff --git a/frontend/src/Store/Selectors/createExistingArtistSelector.js b/frontend/src/Store/Selectors/createExistingArtistSelector.js index 4811f2034..0e7dd11e6 100644 --- a/frontend/src/Store/Selectors/createExistingArtistSelector.js +++ b/frontend/src/Store/Selectors/createExistingArtistSelector.js @@ -4,10 +4,10 @@ import createAllArtistSelector from './createAllArtistSelector'; function createExistingArtistSelector() { return createSelector( - (state, { foreignArtistId }) => foreignArtistId, + (state, { titleSlug }) => titleSlug, createAllArtistSelector(), - (foreignArtistId, artist) => { - return _.some(artist, { foreignArtistId }); + (titleSlug, artist) => { + return _.some(artist, { titleSlug }); } ); } diff --git a/frontend/src/Store/Selectors/createImportArtistItemSelector.js b/frontend/src/Store/Selectors/createImportArtistItemSelector.js index 6d72dc547..8485231b8 100644 --- a/frontend/src/Store/Selectors/createImportArtistItemSelector.js +++ b/frontend/src/Store/Selectors/createImportArtistItemSelector.js @@ -11,7 +11,7 @@ function createImportArtistItemSelector() { (id, addArtist, importArtist, artist) => { const item = _.find(importArtist.items, { id }) || {}; const selectedArtist = item && item.selectedArtist; - const isExistingArtist = !!selectedArtist && _.some(artist, { foreignArtistId: selectedArtist.foreignArtistId }); + const isExistingArtist = !!selectedArtist && _.some(artist, { titleSlug: selectedArtist.titleSlug }); return { defaultMonitor: addArtist.defaults.monitor, diff --git a/frontend/src/Store/Selectors/createQueueItemSelector.js b/frontend/src/Store/Selectors/createQueueItemSelector.js index 089795ced..fda4bf67f 100644 --- a/frontend/src/Store/Selectors/createQueueItemSelector.js +++ b/frontend/src/Store/Selectors/createQueueItemSelector.js @@ -2,16 +2,16 @@ import { createSelector } from 'reselect'; function createQueueItemSelector() { return createSelector( - (state, { albumId }) => albumId, + (state, { bookId }) => bookId, (state) => state.queue.details.items, - (albumId, details) => { - if (!albumId) { + (bookId, details) => { + if (!bookId) { return null; } return details.find((item) => { if (item.album) { - return item.album.id === albumId; + return item.album.id === bookId; } return false; diff --git a/frontend/src/Styles/Variables/colors.js b/frontend/src/Styles/Variables/colors.js index ef4745f6a..375412cee 100644 --- a/frontend/src/Styles/Variables/colors.js +++ b/frontend/src/Styles/Variables/colors.js @@ -1,4 +1,4 @@ -const readarrGreen = '#00A65B'; +const readarrRed = '#ca302d'; module.exports = { textColor: '#515253', @@ -10,15 +10,15 @@ module.exports = { offWhite: '#f5f7fa', blue: '#06f', yellow: '#FFA500', - primaryColor: '#0b8750', + primaryColor: '#5d9cec', selectedColor: '#f9be03', successColor: '#27c24c', dangerColor: '#f05050', warningColor: '#ffa500', - infoColor: readarrGreen, + infoColor: readarrRed, purple: '#7a43b6', pink: '#ff69b4', - readarrGreen, + readarrRed, helpTextColor: '#909293', darkGray: '#888', gray: '#adadad', @@ -27,18 +27,18 @@ module.exports = { // Theme Colors - themeBlue: readarrGreen, - themeAlternateBlue: '#00a65b', - themeRed: '#c4273c', + themeRed: readarrRed, + themeAlternateRed: '#a41726', + themeDarkRed: '#66001a', themeDarkColor: '#353535', - themeLightColor: '#1d563d', + themeLightColor: '#810020', torrentColor: '#00853d', usenetColor: '#17b1d9', // Links defaultLinkHoverColor: '#fff', - linkColor: '#0b8750', + linkColor: '#5d9cec', linkHoverColor: '#1b72e2', // Sidebar @@ -49,10 +49,10 @@ module.exports = { // Toolbar toolbarColor: '#e1e2e3', - toolbarBackgroundColor: '#1d563d', - toolbarMenuItemBackgroundColor: '#4D8069', + toolbarBackgroundColor: '#810020', + toolbarMenuItemBackgroundColor: '#66001a', toolbarMenuItemHoverBackgroundColor: '#353535', - toolbarLabelColor: '#8895aa', + toolbarLabelColor: '#e1e2e3', // Accents borderColor: '#e5e5e5', @@ -75,10 +75,10 @@ module.exports = { defaultHoverBackgroundColor: '#f5f5f5', defaultHoverBorderColor: '#d6d6d6;', - primaryBackgroundColor: '#0b8750', - primaryBorderColor: '#1d563d', - primaryHoverBackgroundColor: '#097948', - primaryHoverBorderColor: '#1D563D;', + primaryBackgroundColor: '#5d9cec', + primaryBorderColor: '#5899eb', + primaryHoverBackgroundColor: '#4b91ea', + primaryHoverBorderColor: '#3483e7;', successBackgroundColor: '#27c24c', successBorderColor: '#26be4a', @@ -115,8 +115,8 @@ module.exports = { // // Toolbar - toobarButtonHoverColor: '#00A65B', - toobarButtonSelectedColor: '#00A65B', + toobarButtonHoverColor: '#ca302d', + toobarButtonSelectedColor: '#ca302d', // // Scroller @@ -152,7 +152,7 @@ module.exports = { // // Slider - sliderAccentColor: '#0b8750', + sliderAccentColor: '#5d9cec', // // Form diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.js b/frontend/src/System/Status/MoreInfo/MoreInfo.js index 1a1716d63..79de2257e 100644 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.js +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.js @@ -16,12 +16,12 @@ class MoreInfo extends Component { Home page - readarr.audio + readarr.com Wiki - wiki.readarr.audio + wiki.readarr.com Reddit @@ -31,7 +31,7 @@ class MoreInfo extends Component { Discord - #readarr on Discord + #readarr on Discord Donations diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModal.js b/frontend/src/TrackFile/Editor/TrackFileEditorModal.js deleted file mode 100644 index 7f52aca05..000000000 --- a/frontend/src/TrackFile/Editor/TrackFileEditorModal.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import TrackFileEditorModalContentConnector from './TrackFileEditorModalContentConnector'; - -function TrackFileEditorModal(props) { - const { - isOpen, - onModalClose, - ...otherProps - } = props; - - return ( - - { - isOpen && - - } - - ); -} - -TrackFileEditorModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default TrackFileEditorModal; diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorRow.js b/frontend/src/TrackFile/Editor/TrackFileEditorRow.js index 5c00e6858..89594f1de 100644 --- a/frontend/src/TrackFile/Editor/TrackFileEditorRow.js +++ b/frontend/src/TrackFile/Editor/TrackFileEditorRow.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React from 'react'; -import padNumber from 'Utilities/Number/padNumber'; import TableRow from 'Components/Table/TableRow'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; @@ -9,7 +8,6 @@ import TrackQuality from 'Album/TrackQuality'; function TrackFileEditorRow(props) { const { id, - trackNumber, path, quality, isSelected, @@ -23,11 +21,6 @@ function TrackFileEditorRow(props) { isSelected={isSelected} onSelectedChange={onSelectedChange} /> - - - {padNumber(trackNumber, 2)} - - {path} @@ -43,7 +36,6 @@ function TrackFileEditorRow(props) { TrackFileEditorRow.propTypes = { id: PropTypes.number.isRequired, - trackNumber: PropTypes.string.isRequired, path: PropTypes.string.isRequired, quality: PropTypes.object.isRequired, isSelected: PropTypes.bool, diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorTable.js b/frontend/src/TrackFile/Editor/TrackFileEditorTable.js new file mode 100644 index 000000000..a9fa58846 --- /dev/null +++ b/frontend/src/TrackFile/Editor/TrackFileEditorTable.js @@ -0,0 +1,16 @@ +import React from 'react'; +import TrackFileEditorTableContentConnector from './TrackFileEditorTableContentConnector'; + +function TrackFileEditorTable(props) { + const { + ...otherProps + } = props; + + return ( + + ); +} + +export default TrackFileEditorTable; diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.css b/frontend/src/TrackFile/Editor/TrackFileEditorTableContent.css similarity index 100% rename from frontend/src/TrackFile/Editor/TrackFileEditorModalContent.css rename to frontend/src/TrackFile/Editor/TrackFileEditorTableContent.css diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js b/frontend/src/TrackFile/Editor/TrackFileEditorTableContent.js similarity index 59% rename from frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js rename to frontend/src/TrackFile/Editor/TrackFileEditorTableContent.js index ebc6ad892..c1ec18137 100644 --- a/frontend/src/TrackFile/Editor/TrackFileEditorModalContent.js +++ b/frontend/src/TrackFile/Editor/TrackFileEditorTableContent.js @@ -8,25 +8,15 @@ import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; import { kinds } from 'Helpers/Props'; import ConfirmModal from 'Components/Modal/ConfirmModal'; -import Button from 'Components/Link/Button'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import SpinnerButton from 'Components/Link/SpinnerButton'; import SelectInput from 'Components/Form/SelectInput'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalFooter from 'Components/Modal/ModalFooter'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import TrackFileEditorRow from './TrackFileEditorRow'; -import styles from './TrackFileEditorModalContent.css'; +import styles from './TrackFileEditorTableContent.css'; const columns = [ - { - name: 'trackNumber', - label: 'Track', - isVisible: true - }, { name: 'path', label: 'Path', @@ -39,7 +29,7 @@ const columns = [ } ]; -class TrackFileEditorModalContent extends Component { +class TrackFileEditorTableContent extends Component { // // Lifecycle @@ -127,8 +117,7 @@ class TrackFileEditorModalContent extends Component { isPopulated, error, items, - qualities, - onModalClose + qualities } = this.props; const { @@ -150,88 +139,74 @@ class TrackFileEditorModalContent extends Component { const hasSelectedFiles = this.getSelectedIds().length > 0; return ( - - - Manage Tracks - - - - { - isFetching && !isPopulated ? - : - null - } - - { - !isFetching && error ? -
{error}
: - null - } - - { - isPopulated && !items.length ? -
- No track files to manage. -
: - null - } - - { - isPopulated && items.length ? - - - { - items.map((item) => { - return ( - - ); - }) - } - -
: - null - } -
- - -
- + { + isFetching && !isPopulated ? + : + null + } + + { + !isFetching && error ? +
{error}
: + null + } + + { + isPopulated && !items.length ? +
+ No track files to manage. +
: + null + } + + { + isPopulated && items.length ? + - Delete - - -
- -
- - -
: + null + } + +
+ - Close - - + Delete + + +
+ +
+
- + ); } } -TrackFileEditorModalContent.propTypes = { +TrackFileEditorTableContent.propTypes = { isDeleting: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, @@ -255,8 +230,7 @@ TrackFileEditorModalContent.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, qualities: PropTypes.arrayOf(PropTypes.object).isRequired, onDeletePress: PropTypes.func.isRequired, - onQualityChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired + onQualityChange: PropTypes.func.isRequired }; -export default TrackFileEditorModalContent; +export default TrackFileEditorTableContent; diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js b/frontend/src/TrackFile/Editor/TrackFileEditorTableContentConnector.js similarity index 70% rename from frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js rename to frontend/src/TrackFile/Editor/TrackFileEditorTableContentConnector.js index 406d1a04f..a9d251fb8 100644 --- a/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js +++ b/frontend/src/TrackFile/Editor/TrackFileEditorTableContentConnector.js @@ -9,7 +9,7 @@ import createArtistSelector from 'Store/Selectors/createArtistSelector'; import { deleteTrackFiles, updateTrackFiles } from 'Store/Actions/trackFileActions'; import { fetchTracks, clearTracks } from 'Store/Actions/trackActions'; import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; -import TrackFileEditorModalContent from './TrackFileEditorModalContent'; +import TrackFileEditorTableContent from './TrackFileEditorTableContent'; function createSchemaSelector() { return createSelector( @@ -35,45 +35,19 @@ function createSchemaSelector() { function createMapStateToProps() { return createSelector( - (state, { albumId }) => albumId, - (state) => state.tracks, + (state, { bookId }) => bookId, (state) => state.trackFiles, createSchemaSelector(), createArtistSelector(), ( - albumId, - tracks, + bookId, trackFiles, schema, artist ) => { - const filtered = _.filter(tracks.items, (track) => { - if (albumId >= 0 && track.albumId !== albumId) { - return false; - } - - if (!track.trackFileId) { - return false; - } - - return _.some(trackFiles.items, { id: track.trackFileId }); - }); - - const sorted = _.orderBy(filtered, ['albumId', 'absoluteTrackNumber'], ['desc', 'asc']); - - const items = _.map(sorted, (track) => { - const trackFile = _.find(trackFiles.items, { id: track.trackFileId }); - - return { - path: trackFile.path, - quality: trackFile.quality, - ...track - }; - }); - return { ...schema, - items, + items: trackFiles.items, artistType: artist.artistType, isDeleting: trackFiles.isDeleting, isSaving: trackFiles.isSaving @@ -106,24 +80,15 @@ function createMapDispatchToProps(dispatch, props) { }; } -class TrackFileEditorModalContentConnector extends Component { +class TrackFileEditorTableContentConnector extends Component { // // Lifecycle componentDidMount() { - const artistId = this.props.artistId; - const albumId = this.props.albumId; - - this.props.dispatchFetchTracks({ artistId, albumId }); - this.props.dispatchFetchQualityProfileSchema(); } - componentWillUnmount() { - this.props.dispatchClearTracks(); - } - // // Listeners @@ -152,7 +117,7 @@ class TrackFileEditorModalContentConnector extends Component { } = this.props; return ( - @@ -160,9 +125,9 @@ class TrackFileEditorModalContentConnector extends Component { } } -TrackFileEditorModalContentConnector.propTypes = { - artistId: PropTypes.number.isRequired, - albumId: PropTypes.number, +TrackFileEditorTableContentConnector.propTypes = { + authorId: PropTypes.number.isRequired, + bookId: PropTypes.number, qualities: PropTypes.arrayOf(PropTypes.object).isRequired, dispatchFetchTracks: PropTypes.func.isRequired, dispatchClearTracks: PropTypes.func.isRequired, @@ -170,4 +135,4 @@ TrackFileEditorModalContentConnector.propTypes = { dispatchUpdateTrackFiles: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, createMapDispatchToProps)(TrackFileEditorModalContentConnector); +export default connect(createMapStateToProps, createMapDispatchToProps)(TrackFileEditorTableContentConnector); diff --git a/frontend/src/TrackFile/FileDetails.js b/frontend/src/TrackFile/FileDetails.js index 725a1f0a4..5e40dbc67 100644 --- a/frontend/src/TrackFile/FileDetails.js +++ b/frontend/src/TrackFile/FileDetails.js @@ -135,7 +135,7 @@ function FileDetails(props) { { audioTags.artistMBId !== undefined && { - if (albumIds.indexOf(item.id) > -1) { + if (bookIds.indexOf(item.id) > -1) { result.push({ ...item, ...options diff --git a/frontend/src/Utilities/Artist/monitorOptions.js b/frontend/src/Utilities/Artist/monitorOptions.js index b5e942ae6..a7fa142b3 100644 --- a/frontend/src/Utilities/Artist/monitorOptions.js +++ b/frontend/src/Utilities/Artist/monitorOptions.js @@ -1,10 +1,10 @@ const monitorOptions = [ - { key: 'all', value: 'All Albums' }, - { key: 'future', value: 'Future Albums' }, - { key: 'missing', value: 'Missing Albums' }, - { key: 'existing', value: 'Existing Albums' }, - { key: 'first', value: 'Only First Album' }, - { key: 'latest', value: 'Only Latest Album' }, + { key: 'all', value: 'All Books' }, + { key: 'future', value: 'Future Books' }, + { key: 'missing', value: 'Missing Books' }, + { key: 'existing', value: 'Existing Books' }, + { key: 'first', value: 'Only First Book' }, + { key: 'latest', value: 'Only Latest Book' }, { key: 'none', value: 'None' } ]; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js index e67229e4a..c97900169 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js @@ -83,10 +83,10 @@ class CutoffUnmet extends Component { } onToggleSelectedPress = () => { - const albumIds = this.getSelectedIds(); + const bookIds = this.getSelectedIds(); this.props.batchToggleCutoffUnmetAlbums({ - albumIds, + bookIds, monitored: !getMonitoredValue(this.props) }); } diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js index f1259b258..49c7709fe 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js @@ -61,10 +61,10 @@ class CutoffUnmetConnector extends Component { componentDidUpdate(prevProps) { if (hasDifferentItems(prevProps.items, this.props.items)) { - const albumIds = selectUniqueIds(this.props.items, 'id'); + const bookIds = selectUniqueIds(this.props.items, 'id'); const trackFileIds = selectUniqueIds(this.props.items, 'trackFileId'); - this.props.fetchQueueDetails({ albumIds }); + this.props.fetchQueueDetails({ bookIds }); if (trackFileIds.length) { this.props.fetchTrackFiles({ trackFileIds }); @@ -128,7 +128,7 @@ class CutoffUnmetConnector extends Component { onSearchSelectedPress = (selected) => { this.props.executeCommand({ name: commandNames.ALBUM_SEARCH, - albumIds: selected + bookIds: selected }); } diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index 6cf592fa6..3b89ccf3a 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -17,8 +17,7 @@ function CutoffUnmetRow(props) { trackFileId, artist, releaseDate, - foreignAlbumId, - albumType, + titleSlug, title, disambiguation, isSelected, @@ -49,22 +48,22 @@ function CutoffUnmetRow(props) { return null; } - if (name === 'artist.sortName') { + if (name === 'authors.sortName') { return ( ); } - if (name === 'albumTitle') { + if (name === 'books.title') { return ( @@ -72,14 +71,6 @@ function CutoffUnmetRow(props) { ); } - if (name === 'albumType') { - return ( - - {albumType} - - ); - } - if (name === 'releaseDate') { return ( @@ -108,8 +99,8 @@ function CutoffUnmetRow(props) { return ( { - const albumIds = this.getSelectedIds(); + const bookIds = this.getSelectedIds(); this.props.batchToggleMissingAlbums({ - albumIds, + bookIds, monitored: !getMonitoredValue(this.props) }); } diff --git a/frontend/src/Wanted/Missing/MissingConnector.js b/frontend/src/Wanted/Missing/MissingConnector.js index ec90e274d..3b3622dc4 100644 --- a/frontend/src/Wanted/Missing/MissingConnector.js +++ b/frontend/src/Wanted/Missing/MissingConnector.js @@ -58,8 +58,8 @@ class MissingConnector extends Component { componentDidUpdate(prevProps) { if (hasDifferentItems(prevProps.items, this.props.items)) { - const albumIds = selectUniqueIds(this.props.items, 'id'); - this.props.fetchQueueDetails({ albumIds }); + const bookIds = selectUniqueIds(this.props.items, 'id'); + this.props.fetchQueueDetails({ bookIds }); } } @@ -118,7 +118,7 @@ class MissingConnector extends Component { onSearchSelectedPress = (selected) => { this.props.executeCommand({ name: commandNames.ALBUM_SEARCH, - albumIds: selected + bookIds: selected }); } diff --git a/frontend/src/Wanted/Missing/MissingRow.js b/frontend/src/Wanted/Missing/MissingRow.js index f019c8aca..18830ea82 100644 --- a/frontend/src/Wanted/Missing/MissingRow.js +++ b/frontend/src/Wanted/Missing/MissingRow.js @@ -14,8 +14,7 @@ function MissingRow(props) { id, artist, releaseDate, - albumType, - foreignAlbumId, + titleSlug, title, disambiguation, isSelected, @@ -46,22 +45,22 @@ function MissingRow(props) { return null; } - if (name === 'artist.sortName') { + if (name === 'authors.sortName') { return ( ); } - if (name === 'albumTitle') { + if (name === 'books.title') { return ( @@ -69,14 +68,6 @@ function MissingRow(props) { ); } - if (name === 'albumType') { - return ( - - {albumType} - - ); - } - if (name === 'releaseDate') { return ( CFBundleIconFile readarr.icns CFBundleIdentifier - com.osx.readarr.audio + com.osx.readarr.com CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/package.json b/package.json index cc88cfe28..fd97517ee 100644 --- a/package.json +++ b/package.json @@ -20,93 +20,94 @@ "license": "GPL-3.0", "readmeFilename": "readme.md", "dependencies": { - "@babel/core": "7.5.5", - "@babel/plugin-proposal-class-properties": "7.5.5", - "@babel/plugin-proposal-decorators": "7.4.4", - "@babel/plugin-proposal-export-default-from": "7.5.2", - "@babel/plugin-proposal-export-namespace-from": "7.5.2", - "@babel/plugin-proposal-function-sent": "7.5.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4", - "@babel/plugin-proposal-numeric-separator": "7.2.0", - "@babel/plugin-proposal-optional-chaining": "7.2.0", - "@babel/plugin-proposal-throw-expressions": "7.2.0", - "@babel/plugin-syntax-dynamic-import": "7.2.0", - "@babel/preset-env": "7.5.5", - "@babel/preset-react": "7.0.0", - "@fortawesome/fontawesome-free": "5.10.2", - "@fortawesome/fontawesome-svg-core": "1.2.22", - "@fortawesome/free-regular-svg-icons": "5.10.2", - "@fortawesome/free-solid-svg-icons": "5.10.2", - "@fortawesome/react-fontawesome": "0.1.4", + "@babel/core": "7.7.5", + "@babel/plugin-proposal-class-properties": "7.7.4", + "@babel/plugin-proposal-decorators": "7.7.4", + "@babel/plugin-proposal-export-default-from": "7.7.4", + "@babel/plugin-proposal-export-namespace-from": "7.7.4", + "@babel/plugin-proposal-function-sent": "7.7.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.7.4", + "@babel/plugin-proposal-numeric-separator": "7.7.4", + "@babel/plugin-proposal-optional-chaining": "7.7.5", + "@babel/plugin-proposal-throw-expressions": "7.7.4", + "@babel/plugin-syntax-dynamic-import": "7.7.4", + "@babel/preset-env": "7.7.5", + "@babel/preset-react": "7.7.4", + "@fortawesome/fontawesome-free": "5.11.2", + "@fortawesome/fontawesome-svg-core": "1.2.25", + "@fortawesome/free-regular-svg-icons": "5.11.2", + "@fortawesome/free-solid-svg-icons": "5.11.2", + "@fortawesome/react-fontawesome": "0.1.8", "@microsoft/signalr": "3.1.0", - "@sentry/browser": "5.6.3", - "@sentry/integrations": "5.6.1", + "@sentry/browser": "5.11.0", + "@sentry/integrations": "5.11.0", "ansi-colors": "4.1.1", - "autoprefixer": "9.6.1", + "autoprefixer": "9.7.3", "babel-eslint": "10.0.3", "babel-loader": "8.0.6", "babel-plugin-inline-classnames": "2.0.1", "babel-plugin-transform-react-remove-prop-types": "0.4.24", "classnames": "2.2.6", "clipboard": "2.0.4", - "connected-react-router": "6.5.2", + "connected-react-router": "6.6.1", "core-js": "3", "create-react-class": "15.6.3", - "css-loader": "3.2.0", + "css-loader": "3.2.1", "del": "5.1.0", "element-class": "0.2.2", - "eslint": "6.4.0", + "eslint": "6.8.0", "eslint-plugin-filenames": "1.3.2", - "eslint-plugin-react": "7.14.3", - "esprint": "0.5.0", - "file-loader": "4.2.0", + "eslint-plugin-react": "7.18.0", + "esprint": "0.6.0", + "file-loader": "5.0.2", "filesize": "4.1.2", - "fuse.js": "3.4.5", + "fuse.js": "3.4.6", "gulp": "4.0.2", "gulp-cached": "1.1.1", "gulp-concat": "2.6.1", - "gulp-livereload": "4.0.1", + "gulp-livereload": "4.0.2", "gulp-postcss": "8.0.0", "gulp-print": "5.0.2", "gulp-sourcemaps": "2.6.5", "gulp-watch": "5.0.1", "gulp-wrap": "0.15.0", - "history": "4.9.0", + "history": "4.10.1", "html-webpack-plugin": "3.2.0", "jdu": "1.0.0", "jquery": "3.4.1", "loader-utils": "^1.1.0", "lodash": "4.17.15", "mini-css-extract-plugin": "0.8.0", - "mobile-detect": "1.4.3", + "mobile-detect": "1.4.4", "moment": "2.24.0", "mousetrap": "1.6.3", "normalize.css": "8.0.1", "optimize-css-assets-webpack-plugin": "5.0.3", "postcss-color-function": "4.1.0", "postcss-loader": "3.0.0", - "postcss-mixins": "6.2.2", - "postcss-nested": "4.1.2", + "postcss-mixins": "6.2.3", + "postcss-nested": "4.2.1", "postcss-simple-vars": "5.0.2", "postcss-url": "8.0.0", "prop-types": "15.7.2", - "qs": "6.7.0", + "qs": "6.9.1", "react": "16.8.6", "react-addons-shallow-compare": "15.6.2", "react-async-script": "1.1.1", "react-autosuggest": "9.4.3", "react-custom-scrollbars": "4.2.1", - "react-dnd": "9.3.4", - "react-dnd-html5-backend": "9.3.4", + "react-dnd": "9.5.1", + "react-dnd-html5-backend": "9.5.1", "react-document-title": "2.0.3", "react-dom": "16.8.6", - "react-google-recaptcha": "1.1.0", - "react-lazyload": "2.6.2", + "react-google-recaptcha": "2.0.1", + "react-lazyload": "2.6.5", "react-measure": "1.4.7", - "react-popper": "1.3.4", - "react-redux": "7.1.1", - "react-router-dom": "5.0.1", - "react-slider": "0.11.2", + "react-popper": "1.3.7", + "react-redux": "7.1.3", + "react-router-dom": "5.1.2", + "react-slider": "1.0.1", + "react-tabs": "3.1.0", "react-text-truncate": "0.15.0", "react-virtualized": "9.21.1", "redux": "4.0.4", @@ -118,12 +119,12 @@ "reselect": "4.0.0", "run-sequence": "2.2.1", "streamqueue": "1.1.2", - "style-loader": "0.23.1", - "stylelint": "10.1.0", - "stylelint-order": "3.0.1", + "style-loader": "1.0.1", + "stylelint": "13.0.0", + "stylelint-order": "4.0.0", "uglifyjs-webpack-plugin": "2.2.0", - "url-loader": "2.1.0", - "webpack": "4.39.3", + "url-loader": "3.0.0", + "webpack": "4.41.2", "webpack-stream": "5.2.1", "worker-loader": "2.0.0" }, diff --git a/setup/readarr.iss b/setup/readarr.iss index 5cdcb8d98..e580e73bc 100644 --- a/setup/readarr.iss +++ b/setup/readarr.iss @@ -3,8 +3,8 @@ #define AppName "Readarr" #define AppPublisher "Team Readarr" -#define AppURL "https://readarr.audio/" -#define ForumsURL "https://forums.readarr.audio/" +#define AppURL "https://readarr.com/" +#define ForumsURL "https://forums.readarr.com/" #define AppExeName "Readarr.exe" #define BaseVersion GetEnv('MAJORVERSION') #define BuildNumber GetEnv('MINORVERSION') @@ -15,7 +15,7 @@ ; NOTE: The value of AppId uniquely identifies this application. ; Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{56C1065D-3523-4025-B76D-6F73F67F7F93} +AppId={{EA316CFC-40C5-4104-A7E1-AFA4D42702D8} AppName={#AppName} AppVersion={#BaseVersion} AppPublisher={#AppPublisher} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 501aad4f6..01a5ac2e9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -57,8 +57,8 @@ Readarr - readarr.audio - Copyright 2017-$([System.DateTime]::Now.ToString('yyyy')) readarr.audio (GNU General Public v3) + readarr.com + Copyright 2017-$([System.DateTime]::Now.ToString('yyyy')) readarr.com (GNU General Public v3) 10.0.0.* diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index 8a123fb88..d04c98de1 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -169,14 +169,14 @@ namespace NzbDrone.Common.Test.Http } var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to") - .AddQueryParam("url", $"https://readarr.audio/") + .AddQueryParam("url", $"https://lidarr.audio/") .Build(); request.AllowAutoRedirect = true; var response = Subject.Get(request); response.StatusCode.Should().Be(HttpStatusCode.OK); - response.Content.Should().Contain("Readarr"); + response.Content.Should().Contain("Lidarr"); ExceptionVerification.ExpectedErrors(0); } @@ -222,7 +222,7 @@ namespace NzbDrone.Common.Test.Http { var file = GetTempFilePath(); - Assert.Throws(() => Subject.DownloadFile("https://download.readarr.audio/wrongpath", file)); + Assert.Throws(() => Subject.DownloadFile("https://download.readarr.com/wrongpath", file)); File.Exists(file).Should().BeFalse(); diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs index 6c8c2222c..69d250969 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests [TestCase(@"""DownloadURL"":""https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")] // Spotify Refresh - [TestCase(@"https://spotify.readarr.audio/renew?refresh_token=mySecret")] + [TestCase(@"https://spotify.readarr.com/renew?refresh_token=mySecret")] public void should_clean_message(string message) { var cleansedMessage = CleanseLogMessage.Cleanse(message); diff --git a/src/NzbDrone.Common/Cloud/ReadarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/ReadarrCloudRequestBuilder.cs index 51b41a14e..7a1a20aed 100644 --- a/src/NzbDrone.Common/Cloud/ReadarrCloudRequestBuilder.cs +++ b/src/NzbDrone.Common/Cloud/ReadarrCloudRequestBuilder.cs @@ -14,10 +14,10 @@ namespace NzbDrone.Common.Cloud public ReadarrCloudRequestBuilder() { //TODO: Create Update Endpoint - Services = new HttpRequestBuilder("https://readarr.lidarr.audio/v1/") + Services = new HttpRequestBuilder("https://readarr.servarr.com/v1/") .CreateFactory(); - Search = new HttpRequestBuilder("https://api.lidarr.audio/api/v0.4/{route}") + Search = new HttpRequestBuilder("https://api.readarr.com/v0.2/{route}") .KeepAlive() .CreateFactory(); } diff --git a/src/NzbDrone.Common/Extensions/FuzzyContains.cs b/src/NzbDrone.Common/Extensions/FuzzyContains.cs index e3aad635a..7bbe75b20 100644 --- a/src/NzbDrone.Common/Extensions/FuzzyContains.cs +++ b/src/NzbDrone.Common/Extensions/FuzzyContains.cs @@ -29,13 +29,13 @@ namespace NzbDrone.Common.Extensions { public static int FuzzyFind(this string text, string pattern, double matchProb) { - return match(text, pattern, matchProb).Item1; + return FuzzyMatch(text, pattern, matchProb).Item1; } // return the accuracy of the best match of pattern within text public static double FuzzyContains(this string text, string pattern) { - return match(text, pattern, 0.25).Item2; + return FuzzyMatch(text, pattern, 0.25).Item2; } /** @@ -45,7 +45,7 @@ namespace NzbDrone.Common.Extensions * @param pattern The pattern to search for. * @return Best match index or -1. */ - private static Tuple match(string text, string pattern, double matchThreshold = 0.5) + public static Tuple FuzzyMatch(this string text, string pattern, double matchThreshold = 0.5) { // Check for null inputs not needed since null can't be passed in C#. if (text.Length == 0 || pattern.Length == 0) @@ -65,7 +65,7 @@ namespace NzbDrone.Common.Extensions } // Do a fuzzy compare. - return match_bitap(text, pattern, matchThreshold); + return MatchBitap(text, pattern, matchThreshold); } /** @@ -75,7 +75,7 @@ namespace NzbDrone.Common.Extensions * @param pattern The pattern to search for. * @return Best match index or -1. */ - private static Tuple match_bitap(string text, string pattern, double matchThreshold) + private static Tuple MatchBitap(string text, string pattern, double matchThreshold) { // Initialise the alphabet. Dictionary s = alphabet(pattern); diff --git a/src/NzbDrone.Common/Http/HttpClient.cs b/src/NzbDrone.Common/Http/HttpClient.cs index afc8b9209..43e235321 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Common.Http public interface IHttpClient { HttpResponse Execute(HttpRequest request); - void DownloadFile(string url, string fileName); + void DownloadFile(string url, string fileName, string userAgent = null); HttpResponse Get(HttpRequest request); HttpResponse Get(HttpRequest request) where T : new(); @@ -229,7 +229,7 @@ namespace NzbDrone.Common.Http } } - public void DownloadFile(string url, string fileName) + public void DownloadFile(string url, string fileName, string userAgent = null) { try { @@ -243,7 +243,7 @@ namespace NzbDrone.Common.Http var stopWatch = Stopwatch.StartNew(); var webClient = new GZipWebClient(); - webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent()); + webClient.Headers.Add(HttpRequestHeader.UserAgent, userAgent ?? _userAgentBuilder.GetUserAgent()); webClient.DownloadFile(url, fileName); stopWatch.Stop(); _logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds); diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index e20423063..99da63ede 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -66,13 +66,13 @@ namespace NzbDrone.Common.Instrumentation if (updateClient) { - dsn = "https://2638320e53f74d5a8183b627b39b3261@sentry.radarr.video/17"; + dsn = "https://54ec0b09e5f641508ac581371978bf21@sentry.servarr.com/5"; } else { dsn = RuntimeInfo.IsProduction - ? "https://2638320e53f74d5a8183b627b39b3261@sentry.radarr.video/17" - : "https://2638320e53f74d5a8183b627b39b3261@sentry.radarr.video/17"; + ? "https://038b792802be44b5ae2be4bf7255abbe@sentry.servarr.com/3" + : "https://31e00a6c63ea42c8b5fe70358526a30d@sentry.servarr.com/4"; } var target = new SentryTarget(dsn) diff --git a/src/NzbDrone.Console/Readarr.Console.csproj b/src/NzbDrone.Console/Readarr.Console.csproj index 9bdf34a9c..9f436dd16 100644 --- a/src/NzbDrone.Console/Readarr.Console.csproj +++ b/src/NzbDrone.Console/Readarr.Console.csproj @@ -3,7 +3,7 @@ Exe net462;netcoreapp3.1 - ..\NzbDrone.Host\NzbDrone.ico + ..\NzbDrone.Host\Readarr.ico app.manifest diff --git a/src/NzbDrone.Core.Test/ArtistStatsTests/ArtistStatisticsFixture.cs b/src/NzbDrone.Core.Test/ArtistStatsTests/ArtistStatisticsFixture.cs index 7757ddfc1..7c5d8a0a9 100644 --- a/src/NzbDrone.Core.Test/ArtistStatsTests/ArtistStatisticsFixture.cs +++ b/src/NzbDrone.Core.Test/ArtistStatsTests/ArtistStatisticsFixture.cs @@ -12,55 +12,32 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.ArtistStatsTests { [TestFixture] - public class ArtistStatisticsFixture : DbTest + public class ArtistStatisticsFixture : DbTest { - private Artist _artist; - private Album _album; - private AlbumRelease _release; - private Track _track; - private TrackFile _trackFile; + private Author _artist; + private Book _album; + private BookFile _trackFile; [SetUp] public void Setup() { - _artist = Builder.CreateNew() - .With(a => a.ArtistMetadataId = 10) + _artist = Builder.CreateNew() + .With(a => a.AuthorMetadataId = 10) .BuildNew(); Db.Insert(_artist); - _album = Builder.CreateNew() + _album = Builder.CreateNew() .With(e => e.ReleaseDate = DateTime.Today.AddDays(-5)) - .With(e => e.ArtistMetadataId = 10) + .With(e => e.AuthorMetadataId = 10) .BuildNew(); Db.Insert(_album); - _release = Builder.CreateNew() - .With(e => e.AlbumId = _album.Id) - .With(e => e.Monitored = true) + _trackFile = Builder.CreateNew() + .With(e => e.Artist = _artist) + .With(e => e.Album = _album) + .With(e => e.BookId == _album.Id) + .With(e => e.Quality = new QualityModel(Quality.MP3_320)) .BuildNew(); - Db.Insert(_release); - - _track = Builder.CreateNew() - .With(e => e.TrackFileId = 0) - .With(e => e.Artist = _artist) - .With(e => e.AlbumReleaseId = _release.Id) - .BuildNew(); - - _trackFile = Builder.CreateNew() - .With(e => e.Artist = _artist) - .With(e => e.Album = _album) - .With(e => e.Quality = new QualityModel(Quality.MP3_256)) - .BuildNew(); - } - - private void GivenTrackWithFile() - { - _track.TrackFileId = 1; - } - - private void GivenTrack() - { - Db.Insert(_track); } private void GivenTrackFile() @@ -71,8 +48,6 @@ namespace NzbDrone.Core.Test.ArtistStatsTests [Test] public void should_get_stats_for_artist() { - GivenTrack(); - var stats = Subject.ArtistStatistics(); stats.Should().HaveCount(1); @@ -81,8 +56,6 @@ namespace NzbDrone.Core.Test.ArtistStatsTests [Test] public void should_not_include_unmonitored_track_in_track_count() { - GivenTrack(); - var stats = Subject.ArtistStatistics(); stats.Should().HaveCount(1); @@ -92,8 +65,7 @@ namespace NzbDrone.Core.Test.ArtistStatsTests [Test] public void should_include_unmonitored_track_with_file_in_track_count() { - GivenTrackWithFile(); - GivenTrack(); + GivenTrackFile(); var stats = Subject.ArtistStatistics(); @@ -104,8 +76,6 @@ namespace NzbDrone.Core.Test.ArtistStatsTests [Test] public void should_have_size_on_disk_of_zero_when_no_track_file() { - GivenTrack(); - var stats = Subject.ArtistStatistics(); stats.Should().HaveCount(1); @@ -115,8 +85,6 @@ namespace NzbDrone.Core.Test.ArtistStatsTests [Test] public void should_have_size_on_disk_when_track_file_exists() { - GivenTrackWithFile(); - GivenTrack(); GivenTrackFile(); var stats = Subject.ArtistStatistics(); diff --git a/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs b/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs index cb04da8f4..00be192f0 100644 --- a/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs @@ -19,8 +19,8 @@ namespace NzbDrone.Core.Test.Blacklisting { _blacklist = new Blacklist { - ArtistId = 12345, - AlbumIds = new List { 1 }, + AuthorId = 12345, + BookIds = new List { 1 }, Quality = new QualityModel(Quality.FLAC), SourceTitle = "artist.name.album.title", Date = DateTime.UtcNow @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.Blacklisting { Subject.Insert(_blacklist); - Subject.All().First().AlbumIds.Should().Contain(_blacklist.AlbumIds); + Subject.All().First().BookIds.Should().Contain(_blacklist.BookIds); } [Test] @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.Blacklisting { Subject.Insert(_blacklist); - Subject.BlacklistedByTitle(_blacklist.ArtistId, _blacklist.SourceTitle.ToUpperInvariant()).Should().HaveCount(1); + Subject.BlacklistedByTitle(_blacklist.AuthorId, _blacklist.SourceTitle.ToUpperInvariant()).Should().HaveCount(1); } } } diff --git a/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs b/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs index e90f1e710..6a23d0a4d 100644 --- a/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs @@ -19,8 +19,8 @@ namespace NzbDrone.Core.Test.Blacklisting { _event = new DownloadFailedEvent { - ArtistId = 12345, - AlbumIds = new List { 1 }, + AuthorId = 12345, + BookIds = new List { 1 }, Quality = new QualityModel(Quality.MP3_320), SourceTitle = "artist.name.album.title", DownloadClient = "SabnzbdClient", @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.Blacklisting Subject.Handle(_event); Mocker.GetMock() - .Verify(v => v.Insert(It.Is(b => b.AlbumIds == _event.AlbumIds)), Times.Once()); + .Verify(v => v.Insert(It.Is(b => b.BookIds == _event.BookIds)), Times.Once()); } [Test] @@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.Blacklisting _event.Data.Remove("protocol"); Mocker.GetMock() - .Verify(v => v.Insert(It.Is(b => b.AlbumIds == _event.AlbumIds)), Times.Once()); + .Verify(v => v.Insert(It.Is(b => b.BookIds == _event.BookIds)), Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs index 807057837..c444751bd 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore public void SingleOrDefault_should_return_null_on_empty_db() { Mocker.Resolve() - .OpenConnection().Query("SELECT * FROM Artists") + .OpenConnection().Query("SELECT * FROM Authors") .SingleOrDefault(c => c.CleanName == "SomeTitle") .Should() .BeNull(); diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs index 40bc89410..62fc6a4c7 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs @@ -15,42 +15,13 @@ namespace NzbDrone.Core.Test.Datastore [Test] public void one_to_one() { - var album = Builder.CreateNew() + var album = Builder.CreateNew() .With(c => c.Id = 0) .With(x => x.ReleaseDate = DateTime.UtcNow) .With(x => x.LastInfoSync = DateTime.UtcNow) .With(x => x.Added = DateTime.UtcNow) .BuildNew(); Db.Insert(album); - - var albumRelease = Builder.CreateNew() - .With(c => c.Id = 0) - .With(c => c.AlbumId = album.Id) - .BuildNew(); - Db.Insert(albumRelease); - - var loadedAlbum = Db.Single().Album.Value; - - loadedAlbum.Should().NotBeNull(); - loadedAlbum.Should().BeEquivalentTo(album, - options => options - .IncludingAllRuntimeProperties() - .Excluding(c => c.Artist) - .Excluding(c => c.ArtistId) - .Excluding(c => c.ArtistMetadata) - .Excluding(c => c.AlbumReleases)); - } - - [Test] - public void one_to_one_should_not_query_db_if_foreign_key_is_zero() - { - var track = Builder.CreateNew() - .With(c => c.TrackFileId = 0) - .BuildNew(); - - Db.Insert(track); - - Db.Single().TrackFile.Value.Should().BeNull(); } [Test] @@ -77,7 +48,7 @@ namespace NzbDrone.Core.Test.Datastore .Build().ToList(); history[0].Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)); - history[1].Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)); + history[1].Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)); Db.InsertMany(history); diff --git a/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs index 5241eefcd..f111c4cc8 100644 --- a/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs @@ -29,82 +29,43 @@ namespace NzbDrone.Core.Test.Datastore profile = Db.Insert(profile); - var metadata = Builder.CreateNew() + var metadata = Builder.CreateNew() .With(v => v.Id = 0) .Build(); Db.Insert(metadata); - var artist = Builder.CreateListOfSize(1) + var artist = Builder.CreateListOfSize(1) .All() .With(v => v.Id = 0) .With(v => v.QualityProfileId = profile.Id) - .With(v => v.ArtistMetadataId = metadata.Id) + .With(v => v.AuthorMetadataId = metadata.Id) .BuildListOfNew(); Db.InsertMany(artist); - var albums = Builder.CreateListOfSize(3) + var albums = Builder.CreateListOfSize(3) .All() .With(v => v.Id = 0) - .With(v => v.ArtistMetadataId = metadata.Id) + .With(v => v.AuthorMetadataId = metadata.Id) .BuildListOfNew(); Db.InsertMany(albums); - var releases = new List(); - foreach (var album in albums) - { - releases.Add( - Builder.CreateNew() - .With(v => v.Id = 0) - .With(v => v.AlbumId = album.Id) - .With(v => v.ForeignReleaseId = "test" + album.Id) - .Build()); - } - - Db.InsertMany(releases); - - var trackFiles = Builder.CreateListOfSize(1) + var trackFiles = Builder.CreateListOfSize(1) .All() .With(v => v.Id = 0) - .With(v => v.AlbumId = albums[0].Id) + .With(v => v.BookId = albums[0].Id) .With(v => v.Quality = new QualityModel()) .BuildListOfNew(); Db.InsertMany(trackFiles); - - var tracks = Builder.CreateListOfSize(10) - .All() - .With(v => v.Id = 0) - .With(v => v.TrackFileId = trackFiles[0].Id) - .With(v => v.AlbumReleaseId = releases[0].Id) - .BuildListOfNew(); - - Db.InsertMany(tracks); - } - - [Test] - public void should_lazy_load_artist_for_track() - { - var db = Mocker.Resolve(); - - var tracks = db.All(); - - Assert.IsNotEmpty(tracks); - foreach (var track in tracks) - { - Assert.IsFalse(track.Artist.IsLoaded); - Assert.IsNotNull(track.Artist.Value); - Assert.IsTrue(track.Artist.IsLoaded); - Assert.IsTrue(track.Artist.Value.Metadata.IsLoaded); - } } [Test] public void should_lazy_load_artist_for_trackfile() { var db = Mocker.Resolve(); - var tracks = db.Query(new SqlBuilder()).ToList(); + var tracks = db.Query(new SqlBuilder()).ToList(); Assert.IsNotEmpty(tracks); foreach (var track in tracks) @@ -120,13 +81,13 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_trackfile_if_not_joined() { var db = Mocker.Resolve(); - var tracks = db.Query(new SqlBuilder()).ToList(); + var tracks = db.Query(new SqlBuilder()).ToList(); foreach (var track in tracks) { - Assert.IsFalse(track.TrackFile.IsLoaded); - Assert.IsNotNull(track.TrackFile.Value); - Assert.IsTrue(track.TrackFile.IsLoaded); + Assert.IsFalse(track.BookFiles.IsLoaded); + Assert.IsNotNull(track.BookFiles.Value); + Assert.IsTrue(track.BookFiles.IsLoaded); } } @@ -136,16 +97,13 @@ namespace NzbDrone.Core.Test.Datastore var db = Mocker.Resolve(); var files = MediaFileRepository.Query(db, new SqlBuilder() - .Join((f, t) => f.Id == t.TrackFileId) - .Join((t, a) => t.AlbumId == a.Id) - .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) - .Join((a, m) => a.ArtistMetadataId == m.Id)); + .Join((t, a) => t.BookId == a.Id) + .Join((album, artist) => album.AuthorMetadataId == artist.AuthorMetadataId) + .Join((a, m) => a.AuthorMetadataId == m.Id)); Assert.IsNotEmpty(files); foreach (var file in files) { - Assert.IsTrue(file.Tracks.IsLoaded); - Assert.IsNotEmpty(file.Tracks.Value); Assert.IsTrue(file.Album.IsLoaded); Assert.IsTrue(file.Artist.IsLoaded); Assert.IsTrue(file.Artist.Value.Metadata.IsLoaded); @@ -156,11 +114,11 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_tracks_if_not_joined_to_trackfile() { var db = Mocker.Resolve(); - var files = db.QueryJoined( + var files = db.QueryJoined( new SqlBuilder() - .Join((t, a) => t.AlbumId == a.Id) - .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) - .Join((a, m) => a.ArtistMetadataId == m.Id), + .Join((t, a) => t.BookId == a.Id) + .Join((album, artist) => album.AuthorMetadataId == artist.AuthorMetadataId) + .Join((a, m) => a.AuthorMetadataId == m.Id), (file, album, artist, metadata) => { file.Album = album; @@ -172,40 +130,10 @@ namespace NzbDrone.Core.Test.Datastore Assert.IsNotEmpty(files); foreach (var file in files) { - Assert.IsFalse(file.Tracks.IsLoaded); - Assert.IsNotNull(file.Tracks.Value); - Assert.IsNotEmpty(file.Tracks.Value); - Assert.IsTrue(file.Tracks.IsLoaded); Assert.IsTrue(file.Album.IsLoaded); Assert.IsTrue(file.Artist.IsLoaded); Assert.IsTrue(file.Artist.Value.Metadata.IsLoaded); } } - - [Test] - public void should_lazy_load_tracks_if_not_joined() - { - var db = Mocker.Resolve(); - var release = db.Query(new SqlBuilder().Where(x => x.Id == 1)).SingleOrDefault(); - - Assert.IsFalse(release.Tracks.IsLoaded); - Assert.IsNotNull(release.Tracks.Value); - Assert.IsNotEmpty(release.Tracks.Value); - Assert.IsTrue(release.Tracks.IsLoaded); - } - - [Test] - public void should_lazy_load_track_if_not_joined() - { - var db = Mocker.Resolve(); - var tracks = db.Query(new SqlBuilder()).ToList(); - - foreach (var track in tracks) - { - Assert.IsFalse(track.Tracks.IsLoaded); - Assert.IsNotNull(track.Tracks.Value); - Assert.IsTrue(track.Tracks.IsLoaded); - } - } } } diff --git a/src/NzbDrone.Core.Test/Datastore/TableMapperFixture.cs b/src/NzbDrone.Core.Test/Datastore/TableMapperFixture.cs index 486394ed6..9e94272a7 100644 --- a/src/NzbDrone.Core.Test/Datastore/TableMapperFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/TableMapperFixture.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Test.Datastore public class TypeWithNoMappableProperties { - public Artist Artist { get; set; } + public Author Artist { get; set; } public int ReadOnly { get; private set; } public int WriteOnly { private get; set; } diff --git a/src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs b/src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs index 8938acc6f..402cf3a99 100644 --- a/src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs @@ -22,12 +22,12 @@ namespace NzbDrone.Core.Test.Datastore Mocker.Resolve(); } - private WhereBuilder Where(Expression> filter) + private WhereBuilder Where(Expression> filter) { return new WhereBuilder(filter, true, 0); } - private WhereBuilder WhereMetadata(Expression> filter) + private WhereBuilder WhereMetadata(Expression> filter) { return new WhereBuilder(filter, true, 0); } @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.Datastore { _subject = Where(x => x.Id == 10); - _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)"); _subject.Parameters.Get("Clause1_P1").Should().Be(10); } @@ -47,18 +47,18 @@ namespace NzbDrone.Core.Test.Datastore var id = 10; _subject = Where(x => x.Id == id); - _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)"); _subject.Parameters.Get("Clause1_P1").Should().Be(id); } [Test] public void where_equal_property() { - var artist = new Artist { Id = 10 }; + var artist = new Author { Id = 10 }; _subject = Where(x => x.Id == artist.Id); _subject.Parameters.ParameterNames.Should().HaveCount(1); - _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)"); _subject.Parameters.Get("Clause1_P1").Should().Be(artist.Id); } @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.Datastore [Test] public void where_throws_without_concrete_condition_if_requiresConcreteCondition() { - Expression> filter = (x, y) => x.Id == y.Id; + Expression> filter = (x, y) => x.Id == y.Id; _subject = new WhereBuilder(filter, true, 0); Assert.Throws(() => _subject.ToString()); } @@ -83,9 +83,9 @@ namespace NzbDrone.Core.Test.Datastore [Test] public void where_allows_abstract_condition_if_not_requiresConcreteCondition() { - Expression> filter = (x, y) => x.Id == y.Id; + Expression> filter = (x, y) => x.Id == y.Id; _subject = new WhereBuilder(filter, false, 0); - _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = \"Artists\".\"Id\")"); + _subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")"); } [Test] @@ -93,7 +93,7 @@ namespace NzbDrone.Core.Test.Datastore { _subject = Where(x => x.CleanName == null); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" IS NULL)"); + _subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)"); } [Test] @@ -102,16 +102,16 @@ namespace NzbDrone.Core.Test.Datastore string imdb = null; _subject = Where(x => x.CleanName == imdb); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" IS NULL)"); + _subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)"); } [Test] public void where_equal_null_property() { - var artist = new Artist { CleanName = null }; + var artist = new Author { CleanName = null }; _subject = Where(x => x.CleanName == artist.CleanName); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" IS NULL)"); + _subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)"); } [Test] @@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.Datastore var test = "small"; _subject = Where(x => x.CleanName.Contains(test)); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" LIKE '%' || @Clause1_P1 || '%')"); + _subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" LIKE '%' || @Clause1_P1 || '%')"); _subject.Parameters.Get("Clause1_P1").Should().Be(test); } @@ -130,7 +130,7 @@ namespace NzbDrone.Core.Test.Datastore var test = "small"; _subject = Where(x => test.Contains(x.CleanName)); - _subject.ToString().Should().Be($"(@Clause1_P1 LIKE '%' || \"Artists\".\"CleanName\" || '%')"); + _subject.ToString().Should().Be($"(@Clause1_P1 LIKE '%' || \"Authors\".\"CleanName\" || '%')"); _subject.Parameters.Get("Clause1_P1").Should().Be(test); } @@ -140,7 +140,7 @@ namespace NzbDrone.Core.Test.Datastore var test = "small"; _subject = Where(x => x.CleanName.StartsWith(test)); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" LIKE @Clause1_P1 || '%')"); + _subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" LIKE @Clause1_P1 || '%')"); _subject.Parameters.Get("Clause1_P1").Should().Be(test); } @@ -150,7 +150,7 @@ namespace NzbDrone.Core.Test.Datastore var test = "small"; _subject = Where(x => x.CleanName.EndsWith(test)); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" LIKE '%' || @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" LIKE '%' || @Clause1_P1)"); _subject.Parameters.Get("Clause1_P1").Should().Be(test); } @@ -160,10 +160,9 @@ namespace NzbDrone.Core.Test.Datastore var list = new List { 1, 2, 3 }; _subject = Where(x => list.Contains(x.Id)); - _subject.ToString().Should().Be($"(\"Artists\".\"Id\" IN @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"Authors\".\"Id\" IN (1, 2, 3))"); - var param = _subject.Parameters.Get>("Clause1_P1"); - param.Should().BeEquivalentTo(list); + _subject.Parameters.ParameterNames.Should().BeEmpty(); } [Test] @@ -172,7 +171,7 @@ namespace NzbDrone.Core.Test.Datastore var list = new List { 1, 2, 3 }; _subject = Where(x => x.CleanName == "test" && list.Contains(x.Id)); - _subject.ToString().Should().Be($"((\"Artists\".\"CleanName\" = @Clause1_P1) AND (\"Artists\".\"Id\" IN @Clause1_P2))"); + _subject.ToString().Should().Be($"((\"Authors\".\"CleanName\" = @Clause1_P1) AND (\"Authors\".\"Id\" IN (1, 2, 3)))"); } [Test] @@ -180,7 +179,7 @@ namespace NzbDrone.Core.Test.Datastore { _subject = WhereMetadata(x => x.Status == ArtistStatusType.Continuing); - _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" = @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = @Clause1_P1)"); } [Test] @@ -189,7 +188,7 @@ namespace NzbDrone.Core.Test.Datastore var allowed = new List { ArtistStatusType.Continuing, ArtistStatusType.Ended }; _subject = WhereMetadata(x => allowed.Contains(x.Status)); - _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" IN @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" IN @Clause1_P1)"); } [Test] @@ -198,7 +197,7 @@ namespace NzbDrone.Core.Test.Datastore var allowed = new ArtistStatusType[] { ArtistStatusType.Continuing, ArtistStatusType.Ended }; _subject = WhereMetadata(x => allowed.Contains(x.Status)); - _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" IN @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" IN @Clause1_P1)"); } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs deleted file mode 100644 index 06f3ad8a0..000000000 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Music; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.DecisionEngineTests -{ - [TestFixture] - - public class AcceptableSizeSpecificationFixture : CoreTest - { - private const int HIGH_KBPS_BITRATE = 1600; - private const int TWENTY_MINUTE_EP_MILLIS = 20 * 60 * 1000; - private const int FORTY_FIVE_MINUTE_LP_MILLIS = 45 * 60 * 1000; - private RemoteAlbum _parseResultMultiSet; - private RemoteAlbum _parseResultMulti; - private RemoteAlbum _parseResultSingle; - private Artist _artist; - private QualityDefinition _qualityType; - - private Album AlbumBuilder(int id = 0) - { - return new Album - { - Id = id, - AlbumReleases = new List - { - new AlbumRelease - { - Duration = 0, - Monitored = true - } - } - }; - } - - [SetUp] - public void Setup() - { - _artist = Builder.CreateNew() - .Build(); - - _parseResultMultiSet = new RemoteAlbum - { - Artist = _artist, - Release = new ReleaseInfo(), - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, - Albums = new List { AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder() } - }; - - _parseResultMulti = new RemoteAlbum - { - Artist = _artist, - Release = new ReleaseInfo(), - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, - Albums = new List { AlbumBuilder(), AlbumBuilder() } - }; - - _parseResultSingle = new RemoteAlbum - { - Artist = _artist, - Release = new ReleaseInfo(), - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, - Albums = new List { AlbumBuilder(2) } - }; - - Mocker.GetMock() - .Setup(v => v.Get(It.IsAny())) - .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); - - _qualityType = Builder.CreateNew() - .With(q => q.MinSize = 150) - .With(q => q.MaxSize = 210) - .With(q => q.Quality = Quality.MP3_192) - .Build(); - - Mocker.GetMock().Setup(s => s.Get(Quality.MP3_192)).Returns(_qualityType); - - Mocker.GetMock().Setup( - s => s.GetAlbumsByArtist(It.IsAny())) - .Returns(new List() - { - AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), - AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder(2), AlbumBuilder() - }); - } - - private void GivenLastAlbum() - { - Mocker.GetMock().Setup( - s => s.GetAlbumsByArtist(It.IsAny())) - .Returns(new List - { - AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), - AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder(), AlbumBuilder(2) - }); - } - - [TestCase(TWENTY_MINUTE_EP_MILLIS, 20, false)] - [TestCase(TWENTY_MINUTE_EP_MILLIS, 25, true)] - [TestCase(TWENTY_MINUTE_EP_MILLIS, 35, false)] - [TestCase(FORTY_FIVE_MINUTE_LP_MILLIS, 45, false)] - [TestCase(FORTY_FIVE_MINUTE_LP_MILLIS, 55, true)] - [TestCase(FORTY_FIVE_MINUTE_LP_MILLIS, 75, false)] - public void single_album(int runtime, int sizeInMegaBytes, bool expectedResult) - { - _parseResultSingle.Albums.Select(c => - { - c.AlbumReleases.Value[0].Duration = runtime; - return c; - }).ToList(); - _parseResultSingle.Artist = _artist; - _parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes(); - - Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().Be(expectedResult); - } - - [TestCase(TWENTY_MINUTE_EP_MILLIS, 20 * 2, false)] - [TestCase(TWENTY_MINUTE_EP_MILLIS, 25 * 2, true)] - [TestCase(TWENTY_MINUTE_EP_MILLIS, 35 * 2, false)] - [TestCase(FORTY_FIVE_MINUTE_LP_MILLIS, 45 * 2, false)] - [TestCase(FORTY_FIVE_MINUTE_LP_MILLIS, 55 * 2, true)] - [TestCase(FORTY_FIVE_MINUTE_LP_MILLIS, 75 * 2, false)] - public void multi_album(int runtime, int sizeInMegaBytes, bool expectedResult) - { - _parseResultMulti.Albums.Select(c => - { - c.AlbumReleases.Value[0].Duration = runtime; - return c; - }).ToList(); - _parseResultMulti.Artist = _artist; - _parseResultMulti.Release.Size = sizeInMegaBytes.Megabytes(); - - Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().Be(expectedResult); - } - - [TestCase(TWENTY_MINUTE_EP_MILLIS, 20 * 6, false)] - [TestCase(TWENTY_MINUTE_EP_MILLIS, 25 * 6, true)] - [TestCase(TWENTY_MINUTE_EP_MILLIS, 35 * 6, false)] - [TestCase(FORTY_FIVE_MINUTE_LP_MILLIS, 45 * 6, false)] - [TestCase(FORTY_FIVE_MINUTE_LP_MILLIS, 55 * 6, true)] - [TestCase(FORTY_FIVE_MINUTE_LP_MILLIS, 75 * 6, false)] - public void multiset_album(int runtime, int sizeInMegaBytes, bool expectedResult) - { - _parseResultMultiSet.Albums.Select(c => - { - c.AlbumReleases.Value[0].Duration = runtime; - return c; - }).ToList(); - _parseResultMultiSet.Artist = _artist; - _parseResultMultiSet.Release.Size = sizeInMegaBytes.Megabytes(); - - Subject.IsSatisfiedBy(_parseResultMultiSet, null).Accepted.Should().Be(expectedResult); - } - - [Test] - public void should_return_true_if_size_is_zero() - { - GivenLastAlbum(); - _parseResultSingle.Albums.Select(c => - { - c.AlbumReleases.Value[0].Duration = TWENTY_MINUTE_EP_MILLIS; - return c; - }).ToList(); - _parseResultSingle.Artist = _artist; - _parseResultSingle.Release.Size = 0; - _qualityType.MinSize = 150; - _qualityType.MaxSize = 210; - - Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); - } - - [Test] - public void should_return_true_if_unlimited_20_minute() - { - GivenLastAlbum(); - _parseResultSingle.Albums.Select(c => - { - c.AlbumReleases.Value[0].Duration = TWENTY_MINUTE_EP_MILLIS; - return c; - }).ToList(); - _parseResultSingle.Artist = _artist; - _parseResultSingle.Release.Size = (HIGH_KBPS_BITRATE * 128) * (TWENTY_MINUTE_EP_MILLIS / 1000); - _qualityType.MaxSize = null; - - Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); - } - - [Test] - public void should_return_true_if_unlimited_45_minute() - { - GivenLastAlbum(); - _parseResultSingle.Albums.Select(c => - { - c.AlbumReleases.Value[0].Duration = FORTY_FIVE_MINUTE_LP_MILLIS; - return c; - }).ToList(); - _parseResultSingle.Artist = _artist; - _parseResultSingle.Release.Size = (HIGH_KBPS_BITRATE * 128) * (FORTY_FIVE_MINUTE_LP_MILLIS / 1000); - _qualityType.MaxSize = null; - - Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); - } - } -} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/AlreadyImportedSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/AlreadyImportedSpecificationFixture.cs index 3eb866034..104ac354b 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/AlreadyImportedSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/AlreadyImportedSpecificationFixture.cs @@ -22,29 +22,29 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private const int FIRST_ALBUM_ID = 1; private const string TITLE = "Some.Artist-Some.Album-2018-320kbps-CD-Readarr"; - private Artist _artist; + private Author _artist; private QualityModel _mp3; private QualityModel _flac; private RemoteAlbum _remoteAlbum; private List _history; - private TrackFile _firstFile; + private BookFile _firstFile; [SetUp] public void Setup() { - var singleAlbumList = new List + var singleAlbumList = new List { - new Album + new Book { Id = FIRST_ALBUM_ID, Title = "Some Album" } }; - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .Build(); - _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; + _firstFile = new BookFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; _mp3 = new QualityModel(Quality.MP3_320, new Revision(version: 1)); _flac = new QualityModel(Quality.FLAC, new Revision(version: 1)); @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Mocker.GetMock() .Setup(c => c.GetFilesByAlbum(It.IsAny())) - .Returns(new List { _firstFile }); + .Returns(new List { _firstFile }); } private void GivenCdhDisabled() @@ -105,7 +105,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { Mocker.GetMock() .Setup(c => c.GetFilesByAlbum(It.IsAny())) - .Returns(new List { }); + .Returns(new List { }); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index a616b2d97..1c5992556 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -19,10 +19,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.CutoffNotMet( new QualityProfile { - Cutoff = Quality.MP3_256.Id, + Cutoff = Quality.MP3_320.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new List { new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, + new List { new QualityModel(Quality.Unknown, new Revision(version: 2)) }, NoPreferredWordScore).Should().BeTrue(); } @@ -32,10 +32,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.CutoffNotMet( new QualityProfile { - Cutoff = Quality.MP3_256.Id, + Cutoff = Quality.MP3_320.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new List { new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + new List { new QualityModel(Quality.MP3_320, new Revision(version: 2)) }, NoPreferredWordScore).Should().BeFalse(); } @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.CutoffNotMet( new QualityProfile { - Cutoff = Quality.MP3_256.Id, + Cutoff = Quality.AZW3.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new List { new QualityModel(Quality.MP3_320, new Revision(version: 2)) }, diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DiscographySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DiscographySpecificationFixture.cs index f6a87b092..1776836f0 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DiscographySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DiscographySpecificationFixture.cs @@ -20,17 +20,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - var artist = Builder.CreateNew().With(s => s.Id = 1234).Build(); + var artist = Builder.CreateNew().With(s => s.Id = 1234).Build(); _remoteAlbum = new RemoteAlbum { ParsedAlbumInfo = new ParsedAlbumInfo { Discography = true }, - Albums = Builder.CreateListOfSize(3) + Albums = Builder.CreateListOfSize(3) .All() .With(e => e.ReleaseDate = DateTime.UtcNow.AddDays(-8)) - .With(s => s.ArtistId = artist.Id) + .With(s => s.AuthorId = artist.Id) .BuildList(), Artist = artist, Release = new ReleaseInfo @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }; Mocker.GetMock().Setup(s => s.AlbumsBetweenDates(It.IsAny(), It.IsAny(), false)) - .Returns(new List()); + .Returns(new List()); } [Test] diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index 0f5254933..28af919eb 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -59,8 +59,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _reports = new List { new ReleaseInfo { Title = "Coldplay-A Head Full Of Dreams-CD-FLAC-2015-PERFECT" } }; _remoteAlbum = new RemoteAlbum { - Artist = new Artist(), - Albums = new List { new Album() } + Artist = new Author(), + Albums = new List { new Book() } }; Mocker.GetMock() @@ -231,12 +231,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_only_include_reports_for_requested_albums() { - var artist = Builder.CreateNew().Build(); + var artist = Builder.CreateNew().Build(); - var albums = Builder.CreateListOfSize(2) + var albums = Builder.CreateListOfSize(2) .All() - .With(v => v.ArtistId, artist.Id) - .With(v => v.Artist, new LazyLoaded(artist)) + .With(v => v.AuthorId, artist.Id) + .With(v => v.Author, new LazyLoaded(artist)) .BuildList(); var criteria = new ArtistSearchCriteria { Albums = albums.Take(1).ToList() }; @@ -289,7 +289,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenSpecifications(_pass1, _pass2, _pass3); - _remoteAlbum.Albums = new List(); + _remoteAlbum.Albums = new List(); var result = Subject.GetRssDecision(_reports); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/EarlyReleaseSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/EarlyReleaseSpecificationFixture.cs index 25304405c..c0afce914 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/EarlyReleaseSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/EarlyReleaseSpecificationFixture.cs @@ -17,23 +17,23 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [TestFixture] public class EarlyReleaseSpecificationFixture : TestBase { - private Artist _artist; - private Album _album1; - private Album _album2; + private Author _artist; + private Book _album1; + private Book _album2; private RemoteAlbum _remoteAlbum; private IndexerDefinition _indexerDefinition; [SetUp] public void Setup() { - _artist = Builder.CreateNew().With(s => s.Id = 1).Build(); - _album1 = Builder.CreateNew().With(s => s.ReleaseDate = DateTime.Today).Build(); - _album2 = Builder.CreateNew().With(s => s.ReleaseDate = DateTime.Today).Build(); + _artist = Builder.CreateNew().With(s => s.Id = 1).Build(); + _album1 = Builder.CreateNew().With(s => s.ReleaseDate = DateTime.Today).Build(); + _album2 = Builder.CreateNew().With(s => s.ReleaseDate = DateTime.Today).Build(); _remoteAlbum = new RemoteAlbum { Artist = _artist, - Albums = new List { _album1 }, + Albums = new List { _album1 }, Release = new TorrentInfo { IndexerId = 1, diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index b9bfe8b51..3d3fed50a 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private RemoteAlbum _parseResultSingle; private QualityModel _upgradableQuality; private QualityModel _notupgradableQuality; - private Artist _fakeArtist; + private Author _fakeArtist; [SetUp] public void Setup() @@ -37,15 +37,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Mocker.Resolve(); _upgradeHistory = Mocker.Resolve(); - var singleAlbumList = new List { new Album { Id = FIRST_ALBUM_ID } }; - var doubleAlbumList = new List + var singleAlbumList = new List { new Book { Id = FIRST_ALBUM_ID } }; + var doubleAlbumList = new List { - new Album { Id = FIRST_ALBUM_ID }, - new Album { Id = SECOND_ALBUM_ID }, - new Album { Id = 3 } + new Book { Id = FIRST_ALBUM_ID }, + new Book { Id = SECOND_ALBUM_ID }, + new Book { Id = 3 } }; - _fakeArtist = Builder.CreateNew() + _fakeArtist = Builder.CreateNew() .With(c => c.QualityProfile = new QualityProfile { UpgradeAllowed = true, @@ -57,18 +57,18 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _parseResultMulti = new RemoteAlbum { Artist = _fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)) }, Albums = doubleAlbumList }; _parseResultSingle = new RemoteAlbum { Artist = _fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)) }, Albums = singleAlbumList }; - _upgradableQuality = new QualityModel(Quality.MP3_192, new Revision(version: 1)); + _upgradableQuality = new QualityModel(Quality.MP3_320, new Revision(version: 1)); _notupgradableQuality = new QualityModel(Quality.MP3_320, new Revision(version: 2)); Mocker.GetMock() @@ -76,9 +76,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Returns(true); } - private void GivenMostRecentForAlbum(int albumId, string downloadId, QualityModel quality, DateTime date, HistoryEventType eventType) + private void GivenMostRecentForAlbum(int bookId, string downloadId, QualityModel quality, DateTime date, HistoryEventType eventType) { - Mocker.GetMock().Setup(s => s.MostRecentForAlbum(albumId)) + Mocker.GetMock().Setup(s => s.MostRecentForAlbum(bookId)) .Returns(new History.History { DownloadId = downloadId, Quality = quality, Date = date, EventType = eventType }); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredAlbumSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredAlbumSpecificationFixture.cs index 5a2ff60b1..034d22dbd 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredAlbumSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredAlbumSpecificationFixture.cs @@ -18,24 +18,24 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private RemoteAlbum _parseResultMulti; private RemoteAlbum _parseResultSingle; - private Artist _fakeArtist; - private Album _firstAlbum; - private Album _secondAlbum; + private Author _fakeArtist; + private Book _firstAlbum; + private Book _secondAlbum; [SetUp] public void Setup() { _monitoredAlbumSpecification = Mocker.Resolve(); - _fakeArtist = Builder.CreateNew() + _fakeArtist = Builder.CreateNew() .With(c => c.Monitored = true) .Build(); - _firstAlbum = new Album { Monitored = true }; - _secondAlbum = new Album { Monitored = true }; + _firstAlbum = new Book { Monitored = true }; + _secondAlbum = new Book { Monitored = true }; - var singleAlbumList = new List { _firstAlbum }; - var doubleAlbumList = new List { _firstAlbum, _secondAlbum }; + var singleAlbumList = new List { _firstAlbum }; + var doubleAlbumList = new List { _firstAlbum, _secondAlbum }; _parseResultMulti = new RemoteAlbum { diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs index 59102147e..d2893262c 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs @@ -27,20 +27,20 @@ namespace NzbDrone.Core.Test.DecisionEngineTests GivenPreferredDownloadProtocol(DownloadProtocol.Usenet); } - private Album GivenAlbum(int id) + private Book GivenAlbum(int id) { - return Builder.CreateNew() + return Builder.CreateNew() .With(e => e.Id = id) .Build(); } - private RemoteAlbum GivenRemoteAlbum(List albums, QualityModel quality, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet) + private RemoteAlbum GivenRemoteAlbum(List albums, QualityModel quality, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet) { var remoteAlbum = new RemoteAlbum(); remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); remoteAlbum.ParsedAlbumInfo.Quality = quality; - remoteAlbum.Albums = new List(); + remoteAlbum.Albums = new List(); remoteAlbum.Albums.AddRange(albums); remoteAlbum.Release = new ReleaseInfo(); @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests remoteAlbum.Release.Size = size; remoteAlbum.Release.DownloadProtocol = downloadProtocol; - remoteAlbum.Artist = Builder.CreateNew() + remoteAlbum.Artist = Builder.CreateNew() .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() @@ -72,8 +72,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_put_propers_before_non_propers() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 1))); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 2))); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320, new Revision(version: 1))); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320, new Revision(version: 2))); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -86,24 +86,24 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_put_higher_quality_before_lower() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); decisions.Add(new DownloadDecision(remoteAlbum2)); var qualifiedReports = Subject.PrioritizeDecisions(decisions); - qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Quality.Should().Be(Quality.MP3_256); + qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Quality.Should().Be(Quality.MP3_320); } [Test] public void should_order_by_age_then_largest_rounded_to_200mb() { - var remoteAlbumSd = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192), size: 100.Megabytes(), age: 1); - var remoteAlbumHdSmallOld = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 1200.Megabytes(), age: 1000); - var remoteAlbumSmallYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 1250.Megabytes(), age: 10); - var remoteAlbumHdLargeYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 3000.Megabytes(), age: 1); + var remoteAlbumSd = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), size: 100.Megabytes(), age: 1); + var remoteAlbumHdSmallOld = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), size: 1200.Megabytes(), age: 1000); + var remoteAlbumSmallYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), size: 1250.Megabytes(), age: 10); + var remoteAlbumHdLargeYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), size: 3000.Megabytes(), age: 1); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbumSd)); @@ -118,8 +118,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_order_by_youngest() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), age: 10); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), age: 5); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), age: 10); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), age: 5); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -132,10 +132,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_not_throw_if_no_albums_are_found() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 500.Megabytes()); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 500.Megabytes()); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), size: 500.Megabytes()); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), size: 500.Megabytes()); - remoteAlbum1.Albums = new List(); + remoteAlbum1.Albums = new List(); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -149,8 +149,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenPreferredDownloadProtocol(DownloadProtocol.Usenet); - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Torrent); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Usenet); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), downloadProtocol: DownloadProtocol.Torrent); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), downloadProtocol: DownloadProtocol.Usenet); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -165,8 +165,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenPreferredDownloadProtocol(DownloadProtocol.Torrent); - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Torrent); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Usenet); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), downloadProtocol: DownloadProtocol.Torrent); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), downloadProtocol: DownloadProtocol.Usenet); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -179,8 +179,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_discography_pack_above_single_album() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.FLAC)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.FLAC)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC)); remoteAlbum1.ParsedAlbumInfo.Discography = true; @@ -195,8 +195,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_quality_over_discography_pack() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.MP3_320)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC)); remoteAlbum1.ParsedAlbumInfo.Discography = true; @@ -211,8 +211,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_single_album_over_multi_album() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -225,8 +225,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_releases_with_more_seeders() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -251,8 +251,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_releases_with_more_peers_given_equal_number_of_seeds() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -278,8 +278,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_releases_with_more_peers_no_seeds() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -306,8 +306,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_first_release_if_peers_and_size_are_too_similar() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -335,8 +335,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_first_release_if_age_and_size_are_too_similar() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); remoteAlbum1.Release.PublishDate = DateTime.UtcNow.AddDays(-100); remoteAlbum1.Release.Size = 200.Megabytes(); @@ -355,8 +355,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_quality_over_the_number_of_peers() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.AZW3)); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -384,8 +384,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_put_higher_quality_before_lower_always() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -398,8 +398,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_higher_score_over_lower_score() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC)); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC)); remoteAlbum1.PreferredWordScore = 10; remoteAlbum2.PreferredWordScore = 0; @@ -419,8 +419,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Setup(s => s.DownloadPropersAndRepacks) .Returns(ProperDownloadTypes.PreferAndUpgrade); - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1))); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2))); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1))); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2))); remoteAlbum1.PreferredWordScore = 10; remoteAlbum2.PreferredWordScore = 0; @@ -440,8 +440,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Setup(s => s.DownloadPropersAndRepacks) .Returns(ProperDownloadTypes.DoNotUpgrade); - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1))); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2))); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1))); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2))); remoteAlbum1.PreferredWordScore = 10; remoteAlbum2.PreferredWordScore = 0; @@ -461,8 +461,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Setup(s => s.DownloadPropersAndRepacks) .Returns(ProperDownloadTypes.DoNotPrefer); - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1))); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2))); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1))); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2))); remoteAlbum1.PreferredWordScore = 10; remoteAlbum2.PreferredWordScore = 0; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ProtocolSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ProtocolSpecificationFixture.cs index d30d365a5..7b42abfcd 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/ProtocolSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ProtocolSpecificationFixture.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { _remoteAlbum = new RemoteAlbum(); _remoteAlbum.Release = new ReleaseInfo(); - _remoteAlbum.Artist = new Artist(); + _remoteAlbum.Artist = new Author(); _delayProfile = new DelayProfile(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs index f6aa5b3ff..444cbe094 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs @@ -18,14 +18,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public static object[] AllowedTestCases = { - new object[] { Quality.MP3_192 }, - new object[] { Quality.MP3_256 }, + new object[] { Quality.MP3_320 }, + new object[] { Quality.MP3_320 }, new object[] { Quality.MP3_320 } }; public static object[] DeniedTestCases = { - new object[] { Quality.MP3_VBR }, new object[] { Quality.FLAC }, new object[] { Quality.Unknown } }; @@ -33,14 +32,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - var fakeArtist = Builder.CreateNew() + var fakeArtist = Builder.CreateNew() .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.MP3_320.Id }) .Build(); _remoteAlbum = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)) }, }; } @@ -49,7 +48,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_allow_if_quality_is_defined_in_profile(Quality qualityType) { _remoteAlbum.ParsedAlbumInfo.Quality.Quality = qualityType; - _remoteAlbum.Artist.QualityProfile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_192, Quality.MP3_256, Quality.MP3_320); + _remoteAlbum.Artist.QualityProfile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_320, Quality.MP3_320, Quality.MP3_320); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); } @@ -59,7 +58,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_not_allow_if_quality_is_not_defined_in_profile(Quality qualityType) { _remoteAlbum.ParsedAlbumInfo.Quality.Quality = qualityType; - _remoteAlbum.Artist.QualityProfile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_192, Quality.MP3_256, Quality.MP3_320); + _remoteAlbum.Artist.QualityProfile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_320, Quality.MP3_320, Quality.MP3_320); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs index ca1bc44a7..63a1d15bb 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs @@ -17,12 +17,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [TestFixture] public class QueueSpecificationFixture : CoreTest { - private Artist _artist; - private Album _album; + private Author _artist; + private Book _album; private RemoteAlbum _remoteAlbum; - private Artist _otherArtist; - private Album _otherAlbum; + private Author _otherArtist; + private Book _otherAlbum; private ReleaseInfo _releaseInfo; @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { Mocker.Resolve(); - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(e => e.QualityProfile = new QualityProfile { UpgradeAllowed = true, @@ -39,16 +39,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }) .Build(); - _album = Builder.CreateNew() - .With(e => e.ArtistId = _artist.Id) + _album = Builder.CreateNew() + .With(e => e.AuthorId = _artist.Id) .Build(); - _otherArtist = Builder.CreateNew() + _otherArtist = Builder.CreateNew() .With(s => s.Id = 2) .Build(); - _otherAlbum = Builder.CreateNew() - .With(e => e.ArtistId = _otherArtist.Id) + _otherAlbum = Builder.CreateNew() + .With(e => e.AuthorId = _otherArtist.Id) .With(e => e.Id = 2) .Build(); @@ -57,8 +57,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album }) - .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256) }) + .With(r => r.Albums = new List { _album }) + .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320) }) .With(r => r.PreferredWordScore = 0) .Build(); } @@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _otherArtist) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .With(r => r.Release = _releaseInfo) .Build(); @@ -110,10 +110,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_320) }) .With(r => r.Release = _releaseInfo) .Build(); @@ -130,10 +130,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_192) + Quality = new QualityModel(Quality.AZW3) }) .With(r => r.Release = _releaseInfo) .Build(); @@ -147,10 +147,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _otherAlbum }) + .With(r => r.Albums = new List { _otherAlbum }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_192) + Quality = new QualityModel(Quality.MP3_320) }) .With(r => r.Release = _releaseInfo) .Build(); @@ -166,10 +166,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_320) }) .With(r => r.Release = _releaseInfo) .Build(); @@ -183,10 +183,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_192) + Quality = new QualityModel(Quality.MP3_320) }) .With(r => r.Release = _releaseInfo) .Build(); @@ -202,7 +202,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320) @@ -219,7 +219,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album, _otherAlbum }) + .With(r => r.Albums = new List { _album, _otherAlbum }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320) @@ -236,7 +236,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320) @@ -255,7 +255,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album, _otherAlbum }) + .With(r => r.Albums = new List { _album, _otherAlbum }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320) @@ -281,9 +281,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }) .With(r => r.Release = _releaseInfo) .TheFirst(1) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .TheNext(1) - .With(r => r.Albums = new List { _otherAlbum }) + .With(r => r.Albums = new List { _otherAlbum }) .Build(); _remoteAlbum.Albums.Add(_otherAlbum); @@ -299,7 +299,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.FLAC) @@ -318,10 +318,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) - .With(r => r.Albums = new List { _album }) + .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_008) + Quality = new QualityModel(Quality.MP3_320) }) .With(r => r.Release = _releaseInfo) .Build(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs index 4d663038c..e95291700 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { _remoteAlbum = new RemoteAlbum { - Artist = new Artist + Artist = new Author { Tags = new HashSet() }, diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs index 7992c2e08..c8174c81f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs @@ -17,8 +17,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public class RepackSpecificationFixture : CoreTest { private ParsedAlbumInfo _parsedAlbumInfo; - private List _albums; - private List _trackFiles; + private List _albums; + private List _trackFiles; [SetUp] public void Setup() @@ -31,13 +31,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(p => p.ReleaseGroup = "Readarr") .Build(); - _albums = Builder.CreateListOfSize(1) + _albums = Builder.CreateListOfSize(1) .All() .BuildList(); - _trackFiles = Builder.CreateListOfSize(3) + _trackFiles = Builder.CreateListOfSize(3) .All() - .With(t => t.AlbumId = _albums.First().Id) + .With(t => t.BookId = _albums.First().Id) .BuildList(); Mocker.GetMock() @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { Mocker.GetMock() .Setup(c => c.GetFilesByAlbum(It.IsAny())) - .Returns(new List()); + .Returns(new List()); _parsedAlbumInfo.Quality.Revision.IsRepack = true; @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }).ToList(); _trackFiles.Select(c => { - c.Quality = new QualityModel(Quality.MP3_256); + c.Quality = new QualityModel(Quality.MP3_320); return c; }).ToList(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs index 9d5b7a40a..62ce2a7e3 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync .With(d => d.PreferredProtocol = DownloadProtocol.Usenet) .Build(); - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .With(s => s.QualityProfile = _profile) .Build(); @@ -46,21 +46,21 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync .Build(); _profile.Items = new List(); - _profile.Items.Add(new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 }); - _profile.Items.Add(new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 }); + _profile.Items.Add(new QualityProfileQualityItem { Allowed = true, Quality = Quality.PDF }); + _profile.Items.Add(new QualityProfileQualityItem { Allowed = true, Quality = Quality.AZW3 }); _profile.Items.Add(new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 }); - _profile.Cutoff = Quality.MP3_320.Id; + _profile.Cutoff = Quality.AZW3.Id; _remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); _remoteAlbum.Release = new ReleaseInfo(); _remoteAlbum.Release.DownloadProtocol = DownloadProtocol.Usenet; - _remoteAlbum.Albums = Builder.CreateListOfSize(1).Build().ToList(); + _remoteAlbum.Albums = Builder.CreateListOfSize(1).Build().ToList(); Mocker.GetMock() .Setup(s => s.GetFilesByAlbum(It.IsAny())) - .Returns(new List { }); + .Returns(new List { }); Mocker.GetMock() .Setup(s => s.BestForTags(It.IsAny>())) @@ -75,12 +75,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync { Mocker.GetMock() .Setup(s => s.GetFilesByAlbum(It.IsAny())) - .Returns(new List - { - new TrackFile + .Returns(new List { - Quality = quality - } + new BookFile + { + Quality = quality + } }); } @@ -100,7 +100,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_be_false_when_system_invoked_search_and_release_is_younger_than_delay() { - _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_192); + _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MOBI); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; _delayProfile.UsenetDelay = 720; @@ -127,7 +127,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_be_true_when_release_is_older_than_delay() { - _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256); + _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MOBI); _remoteAlbum.Release.PublishDate = DateTime.UtcNow.AddHours(-10); _delayProfile.UsenetDelay = 60; @@ -138,7 +138,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_be_false_when_release_is_younger_than_delay() { - _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_192); + _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MOBI); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; _delayProfile.UsenetDelay = 720; @@ -149,10 +149,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_be_true_when_release_is_a_proper_for_existing_album() { - _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)); + _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; - GivenExistingFile(new QualityModel(Quality.MP3_256)); + GivenExistingFile(new QualityModel(Quality.MP3_320)); GivenUpgradeForExistingFile(); Mocker.GetMock() @@ -167,10 +167,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_be_true_when_release_is_a_real_for_existing_album() { - _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256, new Revision(real: 1)); + _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320, new Revision(real: 1)); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; - GivenExistingFile(new QualityModel(Quality.MP3_256)); + GivenExistingFile(new QualityModel(Quality.MP3_320)); GivenUpgradeForExistingFile(); Mocker.GetMock() @@ -185,10 +185,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_be_false_when_release_is_proper_for_existing_album_of_different_quality() { - _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)); + _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.AZW3, new Revision(version: 2)); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; - GivenExistingFile(new QualityModel(Quality.MP3_192)); + GivenExistingFile(new QualityModel(Quality.PDF)); _delayProfile.UsenetDelay = 720; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs index 38c2cf8fe..bdcc7ac4f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs @@ -23,42 +23,39 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync { private RemoteAlbum _parseResultMulti; private RemoteAlbum _parseResultSingle; - private TrackFile _firstFile; - private TrackFile _secondFile; + private BookFile _firstFile; + private BookFile _secondFile; [SetUp] public void Setup() { _firstFile = - new TrackFile + new BookFile { Id = 1, Path = "/My.Artist.S01E01.mp3", Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now, - AlbumId = 1 + BookId = 1 }; _secondFile = - new TrackFile + new BookFile { Id = 2, Path = "/My.Artist.S01E02.mp3", Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now, - AlbumId = 2 + BookId = 2 }; - var singleAlbumList = new List { new Album { Id = 1 } }; - var doubleAlbumList = new List + var singleAlbumList = new List { new Book { Id = 1 } }; + var doubleAlbumList = new List { - new Album { Id = 1 }, - new Album { Id = 2 } + new Book { Id = 1 }, + new Book { Id = 2 } }; - var firstTrack = new Track { TrackFile = _firstFile, TrackFileId = 1, AlbumId = 1 }; - var secondTrack = new Track { TrackFile = _secondFile, TrackFileId = 2, AlbumId = 2 }; - - var fakeArtist = Builder.CreateNew() + var fakeArtist = Builder.CreateNew() .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.FLAC.Id }) .With(c => c.Path = @"C:\Music\My.Artist".AsOsAgnostic()) .Build(); @@ -66,14 +63,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _parseResultMulti = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)) }, Albums = doubleAlbumList }; _parseResultSingle = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)) }, Albums = singleAlbumList }; @@ -87,14 +84,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync .Returns(enabled); } - private void SetupMediaFile(List files) + private void SetupMediaFile(List files) { Mocker.GetMock() .Setup(v => v.GetFilesByAlbum(It.IsAny())) .Returns(files); } - private void WithExistingFile(TrackFile trackFile) + private void WithExistingFile(BookFile trackFile) { var path = trackFile.Path; @@ -121,7 +118,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync public void should_return_true_if_file_exists() { WithExistingFile(_firstFile); - SetupMediaFile(new List { _firstFile }); + SetupMediaFile(new List { _firstFile }); Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); } @@ -129,7 +126,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_return_false_if_file_is_missing() { - SetupMediaFile(new List { _firstFile }); + SetupMediaFile(new List { _firstFile }); Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); } @@ -138,7 +135,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync { WithExistingFile(_firstFile); WithExistingFile(_secondFile); - SetupMediaFile(new List { _firstFile, _secondFile }); + SetupMediaFile(new List { _firstFile, _secondFile }); Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeTrue(); } @@ -147,7 +144,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync public void should_return_false_if_one_of_multiple_episode_is_missing() { WithExistingFile(_firstFile); - SetupMediaFile(new List { _firstFile, _secondFile }); + SetupMediaFile(new List { _firstFile, _secondFile }); Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs index 0145589dd..6b1fcbb60 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs @@ -23,52 +23,52 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync { private RemoteAlbum _parseResultMulti; private RemoteAlbum _parseResultSingle; - private TrackFile _firstFile; - private TrackFile _secondFile; + private BookFile _firstFile; + private BookFile _secondFile; [SetUp] public void Setup() { Mocker.Resolve(); - _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; - _secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; + _firstFile = new BookFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; + _secondFile = new BookFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; - var singleAlbumList = new List { new Album { }, new Album { } }; - var doubleAlbumList = new List { new Album { }, new Album { }, new Album { } }; + var singleAlbumList = new List { new Book { }, new Book { } }; + var doubleAlbumList = new List { new Book { }, new Book { }, new Book { } }; - var fakeArtist = Builder.CreateNew() + var fakeArtist = Builder.CreateNew() .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.FLAC.Id }) .Build(); Mocker.GetMock() .Setup(c => c.GetFilesByAlbum(It.IsAny())) - .Returns(new List { _firstFile, _secondFile }); + .Returns(new List { _firstFile, _secondFile }); _parseResultMulti = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MOBI, new Revision(version: 2)) }, Albums = doubleAlbumList }; _parseResultSingle = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MOBI, new Revision(version: 2)) }, Albums = singleAlbumList }; } private void WithFirstFileUpgradable() { - _firstFile.Quality = new QualityModel(Quality.MP3_192); + _firstFile.Quality = new QualityModel(Quality.PDF); } [Test] public void should_return_false_when_trackFile_was_added_more_than_7_days_ago() { - _firstFile.Quality.Quality = Quality.MP3_256; + _firstFile.Quality.Quality = Quality.MOBI; _firstFile.DateAdded = DateTime.Today.AddDays(-30); Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); @@ -77,8 +77,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_return_false_when_first_trackFile_was_added_more_than_7_days_ago() { - _firstFile.Quality.Quality = Quality.MP3_256; - _secondFile.Quality.Quality = Quality.MP3_256; + _firstFile.Quality.Quality = Quality.MOBI; + _secondFile.Quality.Quality = Quality.MOBI; _firstFile.DateAdded = DateTime.Today.AddDays(-30); Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeFalse(); @@ -87,8 +87,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_return_false_when_second_trackFile_was_added_more_than_7_days_ago() { - _firstFile.Quality.Quality = Quality.MP3_256; - _secondFile.Quality.Quality = Quality.MP3_256; + _firstFile.Quality.Quality = Quality.MOBI; + _secondFile.Quality.Quality = Quality.MOBI; _secondFile.DateAdded = DateTime.Today.AddDays(-30); Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeFalse(); @@ -119,7 +119,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync .Setup(s => s.DownloadPropersAndRepacks) .Returns(ProperDownloadTypes.DoNotUpgrade); - _firstFile.Quality.Quality = Quality.MP3_256; + _firstFile.Quality.Quality = Quality.MOBI; _firstFile.DateAdded = DateTime.Today; Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); @@ -132,7 +132,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync .Setup(s => s.DownloadPropersAndRepacks) .Returns(ProperDownloadTypes.PreferAndUpgrade); - _firstFile.Quality.Quality = Quality.MP3_256; + _firstFile.Quality.Quality = Quality.MOBI; _firstFile.DateAdded = DateTime.Today; Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); @@ -145,7 +145,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync .Setup(s => s.DownloadPropersAndRepacks) .Returns(ProperDownloadTypes.DoNotPrefer); - _firstFile.Quality.Quality = Quality.MP3_256; + _firstFile.Quality.Quality = Quality.MOBI; _firstFile.DateAdded = DateTime.Today; Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/ArtistSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/ArtistSpecificationFixture.cs index d0d22d4c8..a79045bf8 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/ArtistSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/ArtistSpecificationFixture.cs @@ -12,16 +12,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.Search [TestFixture] public class ArtistSpecificationFixture : TestBase { - private Artist _artist1; - private Artist _artist2; + private Author _artist1; + private Author _artist2; private RemoteAlbum _remoteAlbum = new RemoteAlbum(); private SearchCriteriaBase _searchCriteria = new AlbumSearchCriteria(); [SetUp] public void Setup() { - _artist1 = Builder.CreateNew().With(s => s.Id = 1).Build(); - _artist2 = Builder.CreateNew().With(s => s.Id = 2).Build(); + _artist1 = Builder.CreateNew().With(s => s.Id = 1).Build(); + _artist2 = Builder.CreateNew().With(s => s.Id = 2).Build(); _remoteAlbum.Artist = _artist1; } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs index b139c9969..5480f3a02 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs @@ -15,14 +15,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.Search [TestFixture] public class TorrentSeedingSpecificationFixture : TestBase { - private Artist _artist; + private Author _artist; private RemoteAlbum _remoteAlbum; private IndexerDefinition _indexerDefinition; [SetUp] public void Setup() { - _artist = Builder.CreateNew().With(s => s.Id = 1).Build(); + _artist = Builder.CreateNew().With(s => s.Id = 1).Build(); _remoteAlbum = new RemoteAlbum { diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs index 43e431245..79cd3aac7 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs @@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests UpgradeAllowed = true }, new List { new QualityModel(Quality.MP3_320) }, - new QualityModel(Quality.MP3_256)) + new QualityModel(Quality.MP3_320)) .Should().BeTrue(); } @@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests UpgradeAllowed = false }, new List { new QualityModel(Quality.MP3_320) }, - new QualityModel(Quality.MP3_256)) + new QualityModel(Quality.MP3_320)) .Should().BeTrue(); } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index 7fa65e375..b1cac2179 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using Moq; @@ -15,26 +16,26 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.DecisionEngineTests { [TestFixture] - + [Ignore("Pending Readarr fixes")] public class UpgradeDiskSpecificationFixture : CoreTest { private RemoteAlbum _parseResultMulti; private RemoteAlbum _parseResultSingle; - private TrackFile _firstFile; - private TrackFile _secondFile; + private BookFile _firstFile; + private BookFile _secondFile; [SetUp] public void Setup() { Mocker.Resolve(); - _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; - _secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; + _firstFile = new BookFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; + _secondFile = new BookFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; - var singleAlbumList = new List { new Album { } }; - var doubleAlbumList = new List { new Album { }, new Album { }, new Album { } }; + var singleAlbumList = new List { new Book { BookFiles = new List() } }; + var doubleAlbumList = new List { new Book { BookFiles = new List() }, new Book { BookFiles = new List() }, new Book { BookFiles = new List() } }; - var fakeArtist = Builder.CreateNew() + var fakeArtist = Builder.CreateNew() .With(c => c.QualityProfile = new QualityProfile { UpgradeAllowed = true, @@ -43,45 +44,39 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }) .Build(); - Mocker.GetMock() - .Setup(c => c.TracksWithoutFiles(It.IsAny())) - .Returns(new List()); - Mocker.GetMock() .Setup(c => c.GetFilesByAlbum(It.IsAny())) - .Returns(new List { _firstFile, _secondFile }); + .Returns(new List { _firstFile, _secondFile }); _parseResultMulti = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)) }, Albums = doubleAlbumList }; _parseResultSingle = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)) }, Albums = singleAlbumList }; } private void WithFirstFileUpgradable() { - _firstFile.Quality = new QualityModel(Quality.MP3_192); + _firstFile.Quality = new QualityModel(Quality.MP3_320); } private void WithSecondFileUpgradable() { - _secondFile.Quality = new QualityModel(Quality.MP3_192); + _secondFile.Quality = new QualityModel(Quality.MP3_320); } [Test] public void should_return_true_if_album_has_no_existing_file() { - Mocker.GetMock() - .Setup(c => c.GetFilesByAlbum(It.IsAny())) - .Returns(new List { }); + _parseResultSingle.Albums.First().BookFiles = new List(); Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); } @@ -89,10 +84,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_if_track_is_missing() { - Mocker.GetMock() - .Setup(c => c.TracksWithoutFiles(It.IsAny())) - .Returns(new List { new Track() }); - Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); } @@ -101,15 +92,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); - - Mocker.GetMock() - .Verify(c => c.TracksWithoutFiles(It.IsAny()), Times.Once()); } [Test] public void should_return_true_if_single_album_doesnt_exist_on_disk() { - _parseResultSingle.Albums = new List(); + _parseResultSingle.Albums = new List(); Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs index f284f9688..25f236167 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs @@ -15,11 +15,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { public static object[] IsUpgradeTestCases = { - new object[] { Quality.MP3_192, 1, Quality.MP3_192, 2, Quality.MP3_192, true }, + new object[] { Quality.AZW3, 1, Quality.AZW3, 2, Quality.AZW3, true }, new object[] { Quality.MP3_320, 1, Quality.MP3_320, 2, Quality.MP3_320, true }, - new object[] { Quality.MP3_192, 1, Quality.MP3_192, 1, Quality.MP3_192, false }, - new object[] { Quality.MP3_320, 1, Quality.MP3_256, 2, Quality.MP3_320, false }, - new object[] { Quality.MP3_320, 1, Quality.MP3_256, 2, Quality.MP3_320, false }, + new object[] { Quality.MP3_320, 1, Quality.MP3_320, 1, Quality.MP3_320, false }, + new object[] { Quality.MP3_320, 1, Quality.AZW3, 2, Quality.MP3_320, false }, + new object[] { Quality.MP3_320, 1, Quality.AZW3, 2, Quality.MP3_320, false }, new object[] { Quality.MP3_320, 1, Quality.MP3_320, 1, Quality.MP3_320, false } }; @@ -65,9 +65,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsUpgradable( profile, - new List { new QualityModel(Quality.MP3_256, new Revision(version: 1)) }, + new List { new QualityModel(Quality.MP3_320, new Revision(version: 1)) }, NoPreferredWordScore, - new QualityModel(Quality.MP3_256, new Revision(version: 2)), + new QualityModel(Quality.MP3_320, new Revision(version: 2)), NoPreferredWordScore) .Should().BeTrue(); } @@ -84,9 +84,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsUpgradable( profile, - new List { new QualityModel(Quality.MP3_256, new Revision(version: 1)) }, + new List { new QualityModel(Quality.MP3_320, new Revision(version: 1)) }, NoPreferredWordScore, - new QualityModel(Quality.MP3_256, new Revision(version: 2)), + new QualityModel(Quality.MP3_320, new Revision(version: 2)), NoPreferredWordScore) .Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs index b17b4a4b2..823878262 100644 --- a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs +++ b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs @@ -50,7 +50,7 @@ namespace NzbDrone.Core.Test.DiskSpace GivenArtist(); } - private void GivenArtist(params Artist[] artist) + private void GivenArtist(params Author[] artist) { Mocker.GetMock() .Setup(v => v.GetAllArtists()) @@ -67,7 +67,7 @@ namespace NzbDrone.Core.Test.DiskSpace [Test] public void should_check_diskspace_for_artist_folders() { - GivenArtist(new Artist { Path = _artistFolder1 }); + GivenArtist(new Author { Path = _artistFolder1 }); GivenExistingFolder(_artistFolder1); @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.DiskSpace [Test] public void should_check_diskspace_for_same_root_folder_only_once() { - GivenArtist(new Artist { Path = _artistFolder1 }, new Artist { Path = _artistFolder2 }); + GivenArtist(new Author { Path = _artistFolder1 }, new Author { Path = _artistFolder2 }); GivenExistingFolder(_artistFolder1); GivenExistingFolder(_artistFolder2); diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs index c0195c108..f918e61f4 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ImportFixture.cs @@ -58,19 +58,11 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests .Returns(remoteAlbum.Artist); } - private Album CreateAlbum(int id, int trackCount) + private Book CreateAlbum(int id) { - return new Album + return new Book { - Id = id, - AlbumReleases = new List - { - new AlbumRelease - { - Monitored = true, - TrackCount = trackCount - } - } + Id = id }; } @@ -78,8 +70,8 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests { return new RemoteAlbum { - Artist = new Artist(), - Albums = new List { CreateAlbum(1, 1) } + Artist = new Author(), + Albums = new List { CreateAlbum(1) } }; } @@ -94,7 +86,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests Mocker.GetMock() .Setup(s => s.GetArtist(It.IsAny())) - .Returns((Artist)null); + .Returns((Author)null); Mocker.GetMock() .Setup(s => s.GetArtist("Droned S01E01")) @@ -112,7 +104,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests public void should_not_mark_as_imported_if_all_files_were_rejected() { Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult( @@ -136,7 +128,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests public void should_not_mark_as_imported_if_no_tracks_were_parsed() { Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult( @@ -159,7 +151,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests public void should_not_mark_as_imported_if_all_files_were_skipped() { Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }), "Test Failure"), @@ -176,17 +168,15 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests { GivenArtistMatch(); - _trackedDownload.RemoteAlbum.Albums = new List + _trackedDownload.RemoteAlbum.Albums = new List { - CreateAlbum(1, 3) + CreateAlbum(1) }; Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { - new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })), - new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })), new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })), new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }), "Test Failure") }); @@ -199,20 +189,20 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests [Test] public void should_not_mark_as_imported_if_some_tracks_were_not_imported() { - _trackedDownload.RemoteAlbum.Albums = new List + _trackedDownload.RemoteAlbum.Albums = new List { - CreateAlbum(1, 1), - CreateAlbum(1, 2), - CreateAlbum(1, 1) + CreateAlbum(1), + CreateAlbum(1), + CreateAlbum(1) }; Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })), new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })), - new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })), + new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }), "Test Failure"), new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }), "Test Failure"), new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() }), "Test Failure") }); @@ -236,23 +226,12 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests [Test] public void should_not_mark_as_imported_if_some_of_episodes_were_not_imported_including_history() { - var tracks = Builder.CreateListOfSize(3).BuildList(); - - var releases = Builder.CreateListOfSize(3).All().With(x => x.Monitored = true).With(x => x.TrackCount = 1).BuildList(); - releases[0].Tracks = new List { tracks[0] }; - releases[1].Tracks = new List { tracks[1] }; - releases[2].Tracks = new List { tracks[2] }; - - var albums = Builder.CreateListOfSize(3).BuildList(); - - albums[0].AlbumReleases = new List { releases[0] }; - albums[1].AlbumReleases = new List { releases[1] }; - albums[2].AlbumReleases = new List { releases[2] }; + var albums = Builder.CreateListOfSize(3).BuildList(); _trackedDownload.RemoteAlbum.Albums = albums; Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv" })), @@ -279,13 +258,13 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests [Test] public void should_mark_as_imported_if_all_tracks_were_imported() { - _trackedDownload.RemoteAlbum.Albums = new List + _trackedDownload.RemoteAlbum.Albums = new List { - CreateAlbum(1, 2) + CreateAlbum(1) }; Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult( @@ -305,31 +284,21 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests [Test] public void should_mark_as_imported_if_all_episodes_were_imported_including_history() { - var track1 = new Track { Id = 1 }; - var track2 = new Track { Id = 2 }; - - var releases = Builder.CreateListOfSize(2).All().With(x => x.Monitored = true).With(x => x.TrackCount = 1).BuildList(); - releases[0].Tracks = new List { track1 }; - releases[1].Tracks = new List { track2 }; - - var albums = Builder.CreateListOfSize(2).BuildList(); - - albums[0].AlbumReleases = new List { releases[0] }; - albums[1].AlbumReleases = new List { releases[1] }; + var albums = Builder.CreateListOfSize(2).BuildList(); _trackedDownload.RemoteAlbum.Albums = albums; Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult( new ImportDecision( - new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv", Tracks = new List { track1 } })), + new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv", Album = albums[0] })), new ImportResult( new ImportDecision( - new LocalTrack { Path = @"C:\TestPath\Droned.S01E02.mkv", Tracks = new List { track2 } }), "Test Failure") + new LocalTrack { Path = @"C:\TestPath\Droned.S01E02.mkv", Album = albums[1] }), "Test Failure") }); var history = Builder.CreateListOfSize(2) @@ -354,7 +323,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests GivenABadlyNamedDownload(); Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalTrack { Path = @"C:\TestPath\Droned.S01E01.mkv".AsOsAgnostic() })) diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ProcessFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ProcessFixture.cs index 2884fcd43..ea980c277 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ProcessFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceTests/ProcessFixture.cs @@ -58,8 +58,8 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests { return new RemoteAlbum { - Artist = new Artist(), - Albums = new List { new Album { Id = 1 } } + Artist = new Author(), + Albums = new List { new Book { Id = 1 } } }; } @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests Mocker.GetMock() .Setup(s => s.GetArtist(It.IsAny())) - .Returns((Artist)null); + .Returns((Author)null); Mocker.GetMock() .Setup(s => s.GetArtist("Droned S01E01")) @@ -161,7 +161,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests _trackedDownload.RemoteAlbum.Artist = null; Mocker.GetMock() .Setup(s => s.GetArtist("Drone.S01E01.HDTV")) - .Returns((Artist)null); + .Returns((Author)null); Subject.Check(_trackedDownload); diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index 8d774ec36..17b4456e8 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -30,27 +30,27 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests .Returns>(v => v); } - private Album GetAlbum(int id) + private Book GetAlbum(int id) { - return Builder.CreateNew() + return Builder.CreateNew() .With(e => e.Id = id) .Build(); } - private RemoteAlbum GetRemoteAlbum(List albums, QualityModel quality, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet) + private RemoteAlbum GetRemoteAlbum(List albums, QualityModel quality, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet) { var remoteAlbum = new RemoteAlbum(); remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); remoteAlbum.ParsedAlbumInfo.Quality = quality; - remoteAlbum.Albums = new List(); + remoteAlbum.Albums = new List(); remoteAlbum.Albums.AddRange(albums); remoteAlbum.Release = new ReleaseInfo(); remoteAlbum.Release.DownloadProtocol = downloadProtocol; remoteAlbum.Release.PublishDate = DateTime.UtcNow; - remoteAlbum.Artist = Builder.CreateNew() + remoteAlbum.Artist = Builder.CreateNew() .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); @@ -60,8 +60,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_download_report_if_album_was_not_already_downloaded() { - var albums = new List { GetAlbum(1) }; - var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_192)); + var albums = new List { GetAlbum(1) }; + var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum)); @@ -73,8 +73,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_only_download_album_once() { - var albums = new List { GetAlbum(1) }; - var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_192)); + var albums = new List { GetAlbum(1) }; + var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum)); @@ -88,12 +88,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests public void should_not_download_if_any_album_was_already_downloaded() { var remoteAlbum1 = GetRemoteAlbum( - new List { GetAlbum(1) }, - new QualityModel(Quality.MP3_192)); + new List { GetAlbum(1) }, + new QualityModel(Quality.MP3_320)); var remoteAlbum2 = GetRemoteAlbum( - new List { GetAlbum(1), GetAlbum(2) }, - new QualityModel(Quality.MP3_192)); + new List { GetAlbum(1), GetAlbum(2) }, + new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -106,8 +106,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_return_downloaded_reports() { - var albums = new List { GetAlbum(1) }; - var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_192)); + var albums = new List { GetAlbum(1) }; + var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum)); @@ -119,12 +119,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests public void should_return_all_downloaded_reports() { var remoteAlbum1 = GetRemoteAlbum( - new List { GetAlbum(1) }, - new QualityModel(Quality.MP3_192)); + new List { GetAlbum(1) }, + new QualityModel(Quality.MP3_320)); var remoteAlbum2 = GetRemoteAlbum( - new List { GetAlbum(2) }, - new QualityModel(Quality.MP3_192)); + new List { GetAlbum(2) }, + new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -137,16 +137,16 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests public void should_only_return_downloaded_reports() { var remoteAlbum1 = GetRemoteAlbum( - new List { GetAlbum(1) }, - new QualityModel(Quality.MP3_192)); + new List { GetAlbum(1) }, + new QualityModel(Quality.MP3_320)); var remoteAlbum2 = GetRemoteAlbum( - new List { GetAlbum(2) }, - new QualityModel(Quality.MP3_192)); + new List { GetAlbum(2) }, + new QualityModel(Quality.MP3_320)); var remoteAlbum3 = GetRemoteAlbum( - new List { GetAlbum(2) }, - new QualityModel(Quality.MP3_192)); + new List { GetAlbum(2) }, + new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -159,8 +159,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_not_add_to_downloaded_list_when_download_fails() { - var albums = new List { GetAlbum(1) }; - var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_192)); + var albums = new List { GetAlbum(1) }; + var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum)); @@ -183,8 +183,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_not_grab_if_pending() { - var albums = new List { GetAlbum(1) }; - var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_192)); + var albums = new List { GetAlbum(1) }; + var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary))); @@ -196,8 +196,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_not_add_to_pending_if_album_was_grabbed() { - var albums = new List { GetAlbum(1) }; - var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_192)); + var albums = new List { GetAlbum(1) }; + var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum)); @@ -210,8 +210,8 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_add_to_pending_even_if_already_added_to_pending() { - var albums = new List { GetAlbum(1) }; - var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_192)); + var albums = new List { GetAlbum(1) }; + var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary))); @@ -224,7 +224,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_add_to_failed_if_already_failed_for_that_protocol() { - var albums = new List { GetAlbum(1) }; + var albums = new List { GetAlbum(1) }; var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); var decisions = new List(); @@ -241,7 +241,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_not_add_to_failed_if_failed_for_a_different_protocol() { - var albums = new List { GetAlbum(1) }; + var albums = new List { GetAlbum(1) }; var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), DownloadProtocol.Usenet); var remoteAlbum2 = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), DownloadProtocol.Torrent); @@ -260,7 +260,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests [Test] public void should_add_to_rejected_if_release_unavailable_on_indexer() { - var albums = new List { GetAlbum(1) }; + var albums = new List { GetAlbum(1) }; var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320)); var decisions = new List(); diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs index 6fd6c6042..13be200cb 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs @@ -147,7 +147,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.FullUri == _downloadUrl)), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_filePath), Times.Once()); - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] @@ -161,7 +161,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.FullUri == _downloadUrl)), Times.Never()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_filePath), Times.Never()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Once()); - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] @@ -181,7 +181,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.FullUri == _downloadUrl)), Times.Never()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_filePath), Times.Never()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Once()); - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] @@ -196,7 +196,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.FullUri == _downloadUrl)), Times.Never()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_filePath), Times.Never()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Never()); - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] @@ -211,7 +211,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.FullUri == _downloadUrl)), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_filePath), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Never()); - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] @@ -227,7 +227,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.FullUri == _downloadUrl)), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once()); - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs index 5a7b39945..4b50f04ad 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs @@ -119,7 +119,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.FullUri == _downloadUrl)), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_filePath), Times.Once()); - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] @@ -135,7 +135,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.FullUri == _downloadUrl)), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once()); - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs index 02d657396..1b0468945 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -52,9 +52,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); - remoteAlbum.Albums = new List(); + remoteAlbum.Albums = new List(); - remoteAlbum.Artist = new Artist(); + remoteAlbum.Artist = new Author(); return remoteAlbum; } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs index 3bd28c9bb..7f4cdfcb1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests private void WithFailedDownload() { - Mocker.GetMock().Setup(c => c.DownloadFile(It.IsAny(), It.IsAny())).Throws(new WebException()); + Mocker.GetMock().Setup(c => c.DownloadFile(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new WebException()); } [Test] @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests { Subject.Download(_remoteAlbum); - Mocker.GetMock().Verify(c => c.DownloadFile(_nzbUrl, _nzbPath), Times.Once()); + Mocker.GetMock().Verify(c => c.DownloadFile(_nzbUrl, _nzbPath, null), Times.Once()); } [Test] @@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests Subject.Download(_remoteAlbum); - Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), expectedFilename), Times.Once()); + Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), expectedFilename, null), Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index c3a9be76a..39e5574f6 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests protected void GivenRedirectToTorrent() { var httpHeader = new HttpHeader(); - httpHeader["Location"] = "http://test.readarr.audio/not-a-real-torrent.torrent"; + httpHeader["Location"] = "http://test.readarr.com/not-a-real-torrent.torrent"; Mocker.GetMock() .Setup(s => s.Get(It.Is(h => h.Url.FullUri == _downloadUrl))) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index e97812b79..fa2345ab7 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -354,7 +354,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests .Returns(new SabnzbdAddResponse { Ids = new List { "readarrtest" } }); var remoteAlbum = CreateRemoteAlbum(); - remoteAlbum.Albums = Builder.CreateListOfSize(1) + remoteAlbum.Albums = Builder.CreateListOfSize(1) .All() .With(e => e.ReleaseDate = DateTime.Today) .Build() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs index 39a9c967e..fb0d91b73 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs @@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests protected void GivenRedirectToTorrent() { var httpHeader = new HttpHeader(); - httpHeader["Location"] = "http://test.readarr.audio/not-a-real-torrent.torrent"; + httpHeader["Location"] = "http://test.readarr.com/not-a-real-torrent.torrent"; Mocker.GetMock() .Setup(s => s.Get(It.Is(h => h.Url.ToString() == _downloadUrl))) diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs index 3fd0da73b..21b50228c 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs @@ -34,10 +34,10 @@ namespace NzbDrone.Core.Test.Download .Setup(v => v.GetDownloadClient(It.IsAny())) .Returns(v => _downloadClients.FirstOrDefault(d => d.Protocol == v)); - var episodes = Builder.CreateListOfSize(2) + var episodes = Builder.CreateListOfSize(2) .TheFirst(1).With(s => s.Id = 12) .TheNext(1).With(s => s.Id = 99) - .All().With(s => s.ArtistId = 5) + .All().With(s => s.AuthorId = 5) .Build().ToList(); var releaseInfo = Builder.CreateNew() @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.Download .Build(); _parseResult = Builder.CreateNew() - .With(c => c.Artist = Builder.CreateNew().Build()) + .With(c => c.Artist = Builder.CreateNew().Build()) .With(c => c.Release = releaseInfo) .With(c => c.Albums = episodes) .Build(); diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFailedFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFailedFixture.cs index b18c78a26..cf7f19d42 100644 --- a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFailedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFailedFixture.cs @@ -34,8 +34,8 @@ namespace NzbDrone.Core.Test.Download.FailedDownloadServiceTests var remoteAlbum = new RemoteAlbum { - Artist = new Artist(), - Albums = new List { new Album { Id = 1 } } + Artist = new Author(), + Albums = new List { new Book { Id = 1 } } }; _trackedDownload = Builder.CreateNew() diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFixture.cs index e728a80ee..16b8eb744 100644 --- a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFixture.cs +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceTests/ProcessFixture.cs @@ -34,8 +34,8 @@ namespace NzbDrone.Core.Test.Download.FailedDownloadServiceTests var remoteAlbum = new RemoteAlbum { - Artist = new Artist(), - Albums = new List { new Album { Id = 1 } } + Artist = new Author(), + Albums = new List { new Book { Id = 1 } } }; _trackedDownload = Builder.CreateNew() diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs index f628c86ac..79ac001c4 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs @@ -21,8 +21,8 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests public class AddFixture : CoreTest { private DownloadDecision _temporarilyRejected; - private Artist _artist; - private Album _album; + private Author _artist; + private Book _album; private QualityProfile _profile; private ReleaseInfo _release; private ParsedAlbumInfo _parsedAlbumInfo; @@ -32,19 +32,19 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .Build(); - _album = Builder.CreateNew() + _album = Builder.CreateNew() .Build(); _profile = new QualityProfile { Name = "Test", - Cutoff = Quality.MP3_256.Id, + Cutoff = Quality.MP3_320.Id, Items = new List { - new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 }, + new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 }, new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 }, new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 } }, @@ -55,10 +55,10 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _release = Builder.CreateNew().Build(); _parsedAlbumInfo = Builder.CreateNew().Build(); - _parsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256); + _parsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320); _remoteAlbum = new RemoteAlbum(); - _remoteAlbum.Albums = new List { _album }; + _remoteAlbum.Albums = new List { _album }; _remoteAlbum.Artist = _artist; _remoteAlbum.ParsedAlbumInfo = _parsedAlbumInfo; _remoteAlbum.Release = _release; @@ -72,8 +72,8 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests .Returns(_heldReleases); Mocker.GetMock() - .Setup(s => s.AllByArtistId(It.IsAny())) - .Returns(i => _heldReleases.Where(v => v.ArtistId == i).ToList()); + .Setup(s => s.AllByAuthorId(It.IsAny())) + .Returns(i => _heldReleases.Where(v => v.AuthorId == i).ToList()); Mocker.GetMock() .Setup(s => s.GetArtist(It.IsAny())) @@ -81,11 +81,11 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests Mocker.GetMock() .Setup(s => s.GetArtists(It.IsAny>())) - .Returns(new List { _artist }); + .Returns(new List { _artist }); Mocker.GetMock() .Setup(s => s.GetAlbums(It.IsAny(), _artist, null)) - .Returns(new List { _album }); + .Returns(new List { _album }); Mocker.GetMock() .Setup(s => s.PrioritizeDecisions(It.IsAny>())) @@ -100,7 +100,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests var heldReleases = Builder.CreateListOfSize(1) .All() - .With(h => h.ArtistId = _artist.Id) + .With(h => h.AuthorId = _artist.Id) .With(h => h.Title = title) .With(h => h.Release = release) .With(h => h.Reason = reason) diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs index 04ff61b74..e3a251548 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs @@ -21,8 +21,8 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests public class RemoveGrabbedFixture : CoreTest { private DownloadDecision _temporarilyRejected; - private Artist _artist; - private Album _album; + private Author _artist; + private Book _album; private QualityProfile _profile; private ReleaseInfo _release; private ParsedAlbumInfo _parsedAlbumInfo; @@ -32,19 +32,19 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .Build(); - _album = Builder.CreateNew() + _album = Builder.CreateNew() .Build(); _profile = new QualityProfile { Name = "Test", - Cutoff = Quality.MP3_256.Id, + Cutoff = Quality.MP3_320.Id, Items = new List { - new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 }, + new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 }, new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 }, new QualityProfileQualityItem { Allowed = true, Quality = Quality.FLAC } }, @@ -55,10 +55,10 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _release = Builder.CreateNew().Build(); _parsedAlbumInfo = Builder.CreateNew().Build(); - _parsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256); + _parsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320); _remoteAlbum = new RemoteAlbum(); - _remoteAlbum.Albums = new List { _album }; + _remoteAlbum.Albums = new List { _album }; _remoteAlbum.Artist = _artist; _remoteAlbum.ParsedAlbumInfo = _parsedAlbumInfo; _remoteAlbum.Release = _release; @@ -72,8 +72,8 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests .Returns(_heldReleases); Mocker.GetMock() - .Setup(s => s.AllByArtistId(It.IsAny())) - .Returns(i => _heldReleases.Where(v => v.ArtistId == i).ToList()); + .Setup(s => s.AllByAuthorId(It.IsAny())) + .Returns(i => _heldReleases.Where(v => v.AuthorId == i).ToList()); Mocker.GetMock() .Setup(s => s.GetArtist(It.IsAny())) @@ -81,11 +81,11 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests Mocker.GetMock() .Setup(s => s.GetArtists(It.IsAny>())) - .Returns(new List { _artist }); + .Returns(new List { _artist }); Mocker.GetMock() .Setup(s => s.GetAlbums(It.IsAny(), _artist, null)) - .Returns(new List { _album }); + .Returns(new List { _album }); Mocker.GetMock() .Setup(s => s.PrioritizeDecisions(It.IsAny>())) @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests var heldReleases = Builder.CreateListOfSize(1) .All() - .With(h => h.ArtistId = _artist.Id) + .With(h => h.AuthorId = _artist.Id) .With(h => h.Release = _release.JsonClone()) .With(h => h.ParsedAlbumInfo = parsedEpisodeInfo) .Build(); @@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests [Test] public void should_delete_if_the_grabbed_quality_is_the_higher() { - GivenHeldRelease(new QualityModel(Quality.MP3_192)); + GivenHeldRelease(new QualityModel(Quality.MP3_320)); Subject.Handle(new AlbumGrabbedEvent(_remoteAlbum)); diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemovePendingFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemovePendingFixture.cs index 0784006ac..78d9eb823 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemovePendingFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemovePendingFixture.cs @@ -16,18 +16,18 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests public class RemovePendingFixture : CoreTest { private List _pending; - private Album _album; + private Book _album; [SetUp] public void Setup() { _pending = new List(); - _album = Builder.CreateNew() + _album = Builder.CreateNew() .Build(); Mocker.GetMock() - .Setup(s => s.AllByArtistId(It.IsAny())) + .Setup(s => s.AllByAuthorId(It.IsAny())) .Returns(_pending); Mocker.GetMock() @@ -36,15 +36,15 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests Mocker.GetMock() .Setup(s => s.GetArtist(It.IsAny())) - .Returns(new Artist()); + .Returns(new Author()); Mocker.GetMock() .Setup(s => s.GetArtists(It.IsAny>())) - .Returns(new List { new Artist() }); + .Returns(new List { new Author() }); Mocker.GetMock() - .Setup(s => s.GetAlbums(It.IsAny(), It.IsAny(), null)) - .Returns(new List { _album }); + .Setup(s => s.GetAlbums(It.IsAny(), It.IsAny(), null)) + .Returns(new List { _album }); } private void AddPending(int id, string album) diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs index 7875651a6..3e44836d3 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests public class RemoveRejectedFixture : CoreTest { private DownloadDecision _temporarilyRejected; - private Artist _artist; - private Album _album; + private Author _artist; + private Book _album; private QualityProfile _profile; private ReleaseInfo _release; private ParsedAlbumInfo _parsedAlbumInfo; @@ -32,20 +32,20 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .Build(); - _album = Builder.CreateNew() + _album = Builder.CreateNew() .Build(); _profile = new QualityProfile { Name = "Test", - Cutoff = Quality.MP3_192.Id, + Cutoff = Quality.MP3_320.Id, Items = new List { - new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_192 }, - new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 }, + new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 }, + new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 }, new QualityProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 } }, }; @@ -55,10 +55,10 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _release = Builder.CreateNew().Build(); _parsedAlbumInfo = Builder.CreateNew().Build(); - _parsedAlbumInfo.Quality = new QualityModel(Quality.MP3_192); + _parsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320); _remoteAlbum = new RemoteAlbum(); - _remoteAlbum.Albums = new List { _album }; + _remoteAlbum.Albums = new List { _album }; _remoteAlbum.Artist = _artist; _remoteAlbum.ParsedAlbumInfo = _parsedAlbumInfo; _remoteAlbum.Release = _release; @@ -75,11 +75,11 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests Mocker.GetMock() .Setup(s => s.GetArtists(It.IsAny>())) - .Returns(new List { _artist }); + .Returns(new List { _artist }); Mocker.GetMock() .Setup(s => s.GetAlbums(It.IsAny(), _artist, null)) - .Returns(new List { _album }); + .Returns(new List { _album }); Mocker.GetMock() .Setup(s => s.PrioritizeDecisions(It.IsAny>())) @@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests var heldReleases = Builder.CreateListOfSize(1) .All() - .With(h => h.ArtistId = _artist.Id) + .With(h => h.AuthorId = _artist.Id) .With(h => h.Title = title) .With(h => h.Release = release) .Build(); diff --git a/src/NzbDrone.Core.Test/Download/RedownloadFailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/RedownloadFailedDownloadServiceFixture.cs index 2bed4caf8..f17cdc696 100644 --- a/src/NzbDrone.Core.Test/Download/RedownloadFailedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/RedownloadFailedDownloadServiceFixture.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Test.Download Mocker.GetMock() .Setup(x => x.GetAlbumsByArtist(It.IsAny())) - .Returns(Builder.CreateListOfSize(3).Build() as List); + .Returns(Builder.CreateListOfSize(3).Build() as List); } [Test] @@ -31,8 +31,8 @@ namespace NzbDrone.Core.Test.Download { var failedEvent = new DownloadFailedEvent { - ArtistId = 1, - AlbumIds = new List { 1 }, + AuthorId = 1, + BookIds = new List { 1 }, SkipReDownload = true }; @@ -48,8 +48,8 @@ namespace NzbDrone.Core.Test.Download { var failedEvent = new DownloadFailedEvent { - ArtistId = 1, - AlbumIds = new List { 1 } + AuthorId = 1, + BookIds = new List { 1 } }; Mocker.GetMock() @@ -68,15 +68,15 @@ namespace NzbDrone.Core.Test.Download { var failedEvent = new DownloadFailedEvent { - ArtistId = 1, - AlbumIds = new List { 2 } + AuthorId = 1, + BookIds = new List { 2 } }; Subject.Handle(failedEvent); Mocker.GetMock() - .Verify(x => x.Push(It.Is(c => c.AlbumIds.Count == 1 && - c.AlbumIds[0] == 2), + .Verify(x => x.Push(It.Is(c => c.BookIds.Count == 1 && + c.BookIds[0] == 2), It.IsAny(), It.IsAny()), Times.Once()); @@ -91,16 +91,16 @@ namespace NzbDrone.Core.Test.Download { var failedEvent = new DownloadFailedEvent { - ArtistId = 1, - AlbumIds = new List { 2, 3 } + AuthorId = 1, + BookIds = new List { 2, 3 } }; Subject.Handle(failedEvent); Mocker.GetMock() - .Verify(x => x.Push(It.Is(c => c.AlbumIds.Count == 2 && - c.AlbumIds[0] == 2 && - c.AlbumIds[1] == 3), + .Verify(x => x.Push(It.Is(c => c.BookIds.Count == 2 && + c.BookIds[0] == 2 && + c.BookIds[1] == 3), It.IsAny(), It.IsAny()), Times.Once()); @@ -116,14 +116,14 @@ namespace NzbDrone.Core.Test.Download // note that artist is set to have 3 albums in setup var failedEvent = new DownloadFailedEvent { - ArtistId = 2, - AlbumIds = new List { 1, 2, 3 } + AuthorId = 2, + BookIds = new List { 1, 2, 3 } }; Subject.Handle(failedEvent); Mocker.GetMock() - .Verify(x => x.Push(It.Is(c => c.ArtistId == failedEvent.ArtistId), + .Verify(x => x.Push(It.Is(c => c.AuthorId == failedEvent.AuthorId), It.IsAny(), It.IsAny()), Times.Once()); diff --git a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadAlreadyImportedFixture.cs b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadAlreadyImportedFixture.cs index 0d6567585..71258e544 100644 --- a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadAlreadyImportedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadAlreadyImportedFixture.cs @@ -13,14 +13,14 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads [TestFixture] public class TrackedDownloadAlreadyImportedFixture : CoreTest { - private List _albums; + private List _albums; private TrackedDownload _trackedDownload; private List _historyItems; [SetUp] public void Setup() { - _albums = new List(); + _albums = new List(); var remoteAlbum = Builder.CreateNew() .With(r => r.Albums = _albums) @@ -35,17 +35,17 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads public void GivenEpisodes(int count) { - _albums.AddRange(Builder.CreateListOfSize(count) + _albums.AddRange(Builder.CreateListOfSize(count) .BuildList()); } - public void GivenHistoryForEpisode(Album episode, params HistoryEventType[] eventTypes) + public void GivenHistoryForEpisode(Book episode, params HistoryEventType[] eventTypes) { foreach (var eventType in eventTypes) { _historyItems.Add( Builder.CreateNew() - .With(h => h.AlbumId = episode.Id) + .With(h => h.BookId = episode.Id) .With(h => h.EventType = eventType) .Build()); } diff --git a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs index 9246c195d..ee84ac9e7 100644 --- a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs @@ -28,8 +28,8 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads { DownloadId = "35238", SourceTitle = "Audio Artist - Audio Album [2018 - FLAC]", - ArtistId = 5, - AlbumId = 4, + AuthorId = 5, + BookId = 4, } }); } @@ -41,8 +41,8 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads var remoteAlbum = new RemoteAlbum { - Artist = new Artist() { Id = 5 }, - Albums = new List { new Album { Id = 4 } }, + Artist = new Author() { Id = 5 }, + Albums = new List { new Book { Id = 4 } }, ParsedAlbumInfo = new ParsedAlbumInfo() { AlbumTitle = "Audio Album", @@ -82,8 +82,8 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads var remoteAlbum = new RemoteAlbum { - Artist = new Artist() { Id = 5 }, - Albums = new List { new Album { Id = 4 } }, + Artist = new Author() { Id = 5 }, + Albums = new List { new Book { Id = 4 } }, ParsedAlbumInfo = new ParsedAlbumInfo() { AlbumTitle = "Audio Album", diff --git a/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs deleted file mode 100644 index a5fdc7988..000000000 --- a/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.IO; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Extras.Metadata; -using NzbDrone.Core.Extras.Metadata.Consumers.Roksbox; -using NzbDrone.Core.Music; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Roksbox -{ - [TestFixture] - public class FindMetadataFileFixture : CoreTest - { - private Artist _artist; - - [SetUp] - public void Setup() - { - _artist = Builder.CreateNew() - .With(s => s.Path = @"C:\Test\Music\The.Artist".AsOsAgnostic()) - .Build(); - } - - [Test] - public void should_return_null_if_filename_is_not_handled() - { - var path = Path.Combine(_artist.Path, "file.jpg"); - - Subject.FindMetadataFile(_artist, path).Should().BeNull(); - } - - [TestCase("Specials")] - [TestCase("specials")] - [TestCase("Season 1")] - public void should_return_album_image(string folder) - { - var path = Path.Combine(_artist.Path, folder, folder + ".jpg"); - - Subject.FindMetadataFile(_artist, path).Type.Should().Be(MetadataType.AlbumImage); - } - - [TestCase(".xml", MetadataType.TrackMetadata)] - public void should_return_metadata_for_track_if_valid_file_for_track(string extension, MetadataType type) - { - var path = Path.Combine(_artist.Path, "the.artist.s01e01.track" + extension); - - Subject.FindMetadataFile(_artist, path).Type.Should().Be(type); - } - - [Ignore("Need Updated")] - [TestCase(".xml")] - [TestCase(".jpg")] - public void should_return_null_if_not_valid_file_for_track(string extension) - { - var path = Path.Combine(_artist.Path, "the.artist.track" + extension); - - Subject.FindMetadataFile(_artist, path).Should().BeNull(); - } - - [Test] - public void should_not_return_metadata_if_image_file_is_a_thumb() - { - var path = Path.Combine(_artist.Path, "the.artist.s01e01.track-thumb.jpg"); - - Subject.FindMetadataFile(_artist, path).Should().BeNull(); - } - - [Test] - public void should_return_artist_image_for_folder_jpg_in_artist_folder() - { - var path = Path.Combine(_artist.Path, new DirectoryInfo(_artist.Path).Name + ".jpg"); - - Subject.FindMetadataFile(_artist, path).Type.Should().Be(MetadataType.ArtistImage); - } - } -} diff --git a/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs deleted file mode 100644 index d0ae5ef5c..000000000 --- a/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.IO; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Extras.Metadata; -using NzbDrone.Core.Extras.Metadata.Consumers.Wdtv; -using NzbDrone.Core.Music; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Wdtv -{ - [TestFixture] - public class FindMetadataFileFixture : CoreTest - { - private Artist _artist; - - [SetUp] - public void Setup() - { - _artist = Builder.CreateNew() - .With(s => s.Path = @"C:\Test\Music\The.Artist".AsOsAgnostic()) - .Build(); - } - - [Test] - public void should_return_null_if_filename_is_not_handled() - { - var path = Path.Combine(_artist.Path, "file.jpg"); - - Subject.FindMetadataFile(_artist, path).Should().BeNull(); - } - - [TestCase(".xml", MetadataType.TrackMetadata)] - public void should_return_metadata_for_track_if_valid_file_for_track(string extension, MetadataType type) - { - var path = Path.Combine(_artist.Path, "the.artist.s01e01.track" + extension); - - Subject.FindMetadataFile(_artist, path).Type.Should().Be(type); - } - - [Ignore("Need Updated")] - [TestCase(".xml")] - [TestCase(".metathumb")] - public void should_return_null_if_not_valid_file_for_track(string extension) - { - var path = Path.Combine(_artist.Path, "the.artist.track" + extension); - - Subject.FindMetadataFile(_artist, path).Should().BeNull(); - } - } -} diff --git a/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Xbmc/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Xbmc/FindMetadataFileFixture.cs deleted file mode 100644 index d7dcbba93..000000000 --- a/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Xbmc/FindMetadataFileFixture.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.IO; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Extras.Metadata; -using NzbDrone.Core.Extras.Metadata.Consumers.Xbmc; -using NzbDrone.Core.Music; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Xbmc -{ - [TestFixture] - public class FindMetadataFileFixture : CoreTest - { - private Artist _artist; - - [SetUp] - public void Setup() - { - _artist = Builder.CreateNew() - .With(s => s.Path = @"C:\Test\Music\The.Artist".AsOsAgnostic()) - .Build(); - } - - [Test] - public void should_return_null_if_filename_is_not_handled() - { - var path = Path.Combine(_artist.Path, "file.jpg"); - - Subject.FindMetadataFile(_artist, path).Should().BeNull(); - } - - [Test] - public void should_return_metadata_for_xbmc_nfo() - { - var path = Path.Combine(_artist.Path, "album.nfo"); - - Mocker.GetMock() - .Setup(v => v.IsXbmcNfoFile(path)) - .Returns(true); - - Subject.FindMetadataFile(_artist, path).Type.Should().Be(MetadataType.AlbumMetadata); - - Mocker.GetMock() - .Verify(v => v.IsXbmcNfoFile(It.IsAny()), Times.Once()); - } - - [Test] - public void should_return_null_for_scene_nfo() - { - var path = Path.Combine(_artist.Path, "album.nfo"); - - Mocker.GetMock() - .Setup(v => v.IsXbmcNfoFile(path)) - .Returns(false); - - Subject.FindMetadataFile(_artist, path).Should().BeNull(); - - Mocker.GetMock() - .Verify(v => v.IsXbmcNfoFile(It.IsAny()), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs index 6c0f70cc2..050544d47 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs @@ -20,12 +20,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public class DeleteBadMediaCoversFixture : CoreTest { private List _metadata; - private List _artist; + private List _artist; [SetUp] public void Setup() { - _artist = Builder.CreateListOfSize(1) + _artist = Builder.CreateListOfSize(1) .All() .With(c => c.Path = "C:\\Music\\".AsOsAgnostic()) .Build().ToList(); diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs index 3f40ce639..368c39792 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemotePathMappingCheckFixture.cs @@ -166,7 +166,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public void should_return_ok_on_track_imported_event() { GivenFolderExists(_downloadRootPath); - var importEvent = new TrackImportedEvent(new LocalTrack(), new TrackFile(), new List(), true, new DownloadClientItem()); + var importEvent = new TrackImportedEvent(new LocalTrack(), new BookFile(), new List(), true, new DownloadClientItem()); Subject.Check(importEvent).ShouldBeOk(); } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/RootFolderCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/RootFolderCheckFixture.cs index 2ba2d8206..37da55683 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/RootFolderCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/RootFolderCheckFixture.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { private void GivenMissingRootFolder() { - var artist = Builder.CreateListOfSize(1) + var artist = Builder.CreateListOfSize(1) .Build() .ToList(); @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { Mocker.GetMock() .Setup(s => s.GetAllArtists()) - .Returns(new List()); + .Returns(new List()); Mocker.GetMock() .Setup(s => s.All()) diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs index bf149409d..bc309dad7 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs @@ -30,13 +30,13 @@ namespace NzbDrone.Core.Test.HistoryTests { var historyBluray = Builder.CreateNew() .With(c => c.Quality = new QualityModel(Quality.MP3_320)) - .With(c => c.ArtistId = 12) + .With(c => c.AuthorId = 12) .With(c => c.EventType = HistoryEventType.Grabbed) .BuildNew(); var historyDvd = Builder.CreateNew() - .With(c => c.Quality = new QualityModel(Quality.MP3_192)) - .With(c => c.ArtistId = 12) + .With(c => c.Quality = new QualityModel(Quality.AZW3)) + .With(c => c.AuthorId = 12) .With(c => c.EventType = HistoryEventType.Grabbed) .BuildNew(); diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs index 051cc98cf..6c63200c1 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -34,16 +34,15 @@ namespace NzbDrone.Core.Test.HistoryTests _profileCustom = new QualityProfile { Cutoff = Quality.MP3_320.Id, - Items = QualityFixture.GetDefaultQualities(Quality.MP3_256), + Items = QualityFixture.GetDefaultQualities(Quality.MP3_320), }; } [Test] public void should_use_file_name_for_source_title_if_scene_name_is_null() { - var artist = Builder.CreateNew().Build(); - var tracks = Builder.CreateListOfSize(1).Build().ToList(); - var trackFile = Builder.CreateNew() + var artist = Builder.CreateNew().Build(); + var trackFile = Builder.CreateNew() .With(f => f.SceneName = null) .With(f => f.Artist = artist) .Build(); @@ -51,8 +50,7 @@ namespace NzbDrone.Core.Test.HistoryTests var localTrack = new LocalTrack { Artist = artist, - Album = new Album(), - Tracks = tracks, + Album = new Book(), Path = @"C:\Test\Unsorted\Artist.01.Hymn.mp3" }; @@ -62,7 +60,7 @@ namespace NzbDrone.Core.Test.HistoryTests DownloadId = "abcd" }; - Subject.Handle(new TrackImportedEvent(localTrack, trackFile, new List(), true, downloadClientItem)); + Subject.Handle(new TrackImportedEvent(localTrack, trackFile, new List(), true, downloadClientItem)); Mocker.GetMock() .Verify(v => v.Insert(It.Is(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localTrack.Path)))); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs index 8fc699cfa..335d33500 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var files = Builder.CreateListOfSize(2) .All() .With(m => m.Type = MetadataType.ArtistMetadata) - .With(m => m.ArtistId = 1) + .With(m => m.AuthorId = 1) .BuildListOfNew(); Db.InsertMany(files); @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var files = Builder.CreateListOfSize(2) .All() .With(m => m.Type = MetadataType.ArtistMetadata) - .With(m => m.ArtistId = 1) + .With(m => m.AuthorId = 1) .With(m => m.Consumer = "XbmcMetadata") .BuildListOfNew(); @@ -71,8 +71,8 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var files = Builder.CreateListOfSize(2) .All() .With(m => m.Type = MetadataType.AlbumMetadata) - .With(m => m.ArtistId = 1) - .With(m => m.AlbumId = 1) + .With(m => m.AuthorId = 1) + .With(m => m.BookId = 1) .BuildListOfNew(); Db.InsertMany(files); @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers .All() .With(m => m.Type = MetadataType.AlbumMetadata) .With(m => m.Consumer = "XbmcMetadata") - .With(m => m.ArtistId = 1) + .With(m => m.AuthorId = 1) .BuildListOfNew(); Db.InsertMany(files); @@ -101,8 +101,8 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var files = Builder.CreateListOfSize(2) .All() .With(m => m.Type = MetadataType.AlbumMetadata) - .With(m => m.ArtistId = 1) - .With(m => m.AlbumId = 1) + .With(m => m.AuthorId = 1) + .With(m => m.BookId = 1) .With(m => m.Consumer = "XbmcMetadata") .BuildListOfNew(); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedAlbumsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedAlbumsFixture.cs index df0282a29..512195148 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedAlbumsFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedAlbumsFixture.cs @@ -8,12 +8,12 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Housekeeping.Housekeepers { [TestFixture] - public class CleanupOrphanedAlbumsFixture : DbTest + public class CleanupOrphanedAlbumsFixture : DbTest { [Test] public void should_delete_orphaned_albums() { - var album = Builder.CreateNew() + var album = Builder.CreateNew() .BuildNew(); Db.Insert(album); @@ -24,21 +24,21 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_not_delete_unorphaned_albums() { - var artist = Builder.CreateNew() - .With(e => e.Metadata = new ArtistMetadata { Id = 1 }) + var artist = Builder.CreateNew() + .With(e => e.Metadata = new AuthorMetadata { Id = 1 }) .BuildNew(); Db.Insert(artist); - var albums = Builder.CreateListOfSize(2) + var albums = Builder.CreateListOfSize(2) .TheFirst(1) - .With(e => e.ArtistMetadataId = artist.Metadata.Value.Id) + .With(e => e.AuthorMetadataId = artist.Metadata.Value.Id) .BuildListOfNew(); Db.InsertMany(albums); Subject.Clean(); AllStoredModels.Should().HaveCount(1); - AllStoredModels.Should().Contain(e => e.ArtistMetadataId == artist.Metadata.Value.Id); + AllStoredModels.Should().Contain(e => e.AuthorMetadataId == artist.Metadata.Value.Id); } } } diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs index bf35194c1..1c6d252cf 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedBlacklistFixture.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public void should_delete_orphaned_blacklist_items() { var blacklist = Builder.CreateNew() - .With(h => h.AlbumIds = new List()) + .With(h => h.BookIds = new List()) .With(h => h.Quality = new QualityModel()) .BuildNew(); @@ -29,14 +29,14 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_not_delete_unorphaned_blacklist_items() { - var artist = Builder.CreateNew().BuildNew(); + var artist = Builder.CreateNew().BuildNew(); Db.Insert(artist); var blacklist = Builder.CreateNew() - .With(h => h.AlbumIds = new List()) + .With(h => h.BookIds = new List()) .With(h => h.Quality = new QualityModel()) - .With(b => b.ArtistId = artist.Id) + .With(b => b.AuthorId = artist.Id) .BuildNew(); Db.Insert(blacklist); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedHistoryItemsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedHistoryItemsFixture.cs index 5994b0792..9149d17b6 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedHistoryItemsFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedHistoryItemsFixture.cs @@ -11,16 +11,16 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [TestFixture] public class CleanupOrphanedHistoryItemsFixture : DbTest { - private Artist _artist; - private Album _album; + private Author _artist; + private Book _album; [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .BuildNew(); - _album = Builder.CreateNew() + _album = Builder.CreateNew() .BuildNew(); } @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var history = Builder.CreateNew() .With(h => h.Quality = new QualityModel()) - .With(h => h.AlbumId = _album.Id) + .With(h => h.BookId = _album.Id) .BuildNew(); Db.Insert(history); @@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var history = Builder.CreateNew() .With(h => h.Quality = new QualityModel()) - .With(h => h.ArtistId = _artist.Id) + .With(h => h.AuthorId = _artist.Id) .BuildNew(); Db.Insert(history); @@ -73,16 +73,16 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var history = Builder.CreateListOfSize(2) .All() .With(h => h.Quality = new QualityModel()) - .With(h => h.AlbumId = _album.Id) + .With(h => h.BookId = _album.Id) .TheFirst(1) - .With(h => h.ArtistId = _artist.Id) + .With(h => h.AuthorId = _artist.Id) .BuildListOfNew(); Db.InsertMany(history); Subject.Clean(); AllStoredModels.Should().HaveCount(1); - AllStoredModels.Should().Contain(h => h.ArtistId == _artist.Id); + AllStoredModels.Should().Contain(h => h.AuthorId == _artist.Id); } [Test] @@ -94,16 +94,16 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var history = Builder.CreateListOfSize(2) .All() .With(h => h.Quality = new QualityModel()) - .With(h => h.ArtistId = _artist.Id) + .With(h => h.AuthorId = _artist.Id) .TheFirst(1) - .With(h => h.AlbumId = _album.Id) + .With(h => h.BookId = _album.Id) .BuildListOfNew(); Db.InsertMany(history); Subject.Clean(); AllStoredModels.Should().HaveCount(1); - AllStoredModels.Should().Contain(h => h.AlbumId == _album.Id); + AllStoredModels.Should().Contain(h => h.BookId == _album.Id); } } } diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs index 6a0cb4db2..60f07caab 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs @@ -29,13 +29,13 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_delete_metadata_files_that_dont_have_a_coresponding_album() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); Db.Insert(artist); var metadataFile = Builder.CreateNew() - .With(m => m.ArtistId = artist.Id) + .With(m => m.AuthorId = artist.Id) .With(m => m.TrackFileId = null) .BuildNew(); @@ -47,14 +47,14 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_not_delete_metadata_files_that_have_a_coresponding_artist() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); Db.Insert(artist); var metadataFile = Builder.CreateNew() - .With(m => m.ArtistId = artist.Id) - .With(m => m.AlbumId = null) + .With(m => m.AuthorId = artist.Id) + .With(m => m.BookId = null) .With(m => m.TrackFileId = null) .BuildNew(); @@ -67,18 +67,18 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_not_delete_metadata_files_that_have_a_coresponding_album() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); - var album = Builder.CreateNew() + var album = Builder.CreateNew() .BuildNew(); Db.Insert(artist); Db.Insert(album); var metadataFile = Builder.CreateNew() - .With(m => m.ArtistId = artist.Id) - .With(m => m.AlbumId = album.Id) + .With(m => m.AuthorId = artist.Id) + .With(m => m.BookId = album.Id) .With(m => m.TrackFileId = null) .BuildNew(); @@ -90,18 +90,18 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_delete_metadata_files_that_dont_have_a_coresponding_track_file() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); - var album = Builder.CreateNew() + var album = Builder.CreateNew() .BuildNew(); Db.Insert(artist); Db.Insert(album); var metadataFile = Builder.CreateNew() - .With(m => m.ArtistId = artist.Id) - .With(m => m.AlbumId = album.Id) + .With(m => m.AuthorId = artist.Id) + .With(m => m.BookId = album.Id) .With(m => m.TrackFileId = 10) .BuildNew(); @@ -113,13 +113,13 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_not_delete_metadata_files_that_have_a_coresponding_track_file() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); - var album = Builder.CreateNew() + var album = Builder.CreateNew() .BuildNew(); - var trackFile = Builder.CreateNew() + var trackFile = Builder.CreateNew() .With(h => h.Quality = new QualityModel()) .BuildNew(); @@ -128,8 +128,8 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Db.Insert(trackFile); var metadataFile = Builder.CreateNew() - .With(m => m.ArtistId = artist.Id) - .With(m => m.AlbumId = album.Id) + .With(m => m.AuthorId = artist.Id) + .With(m => m.BookId = album.Id) .With(m => m.TrackFileId = trackFile.Id) .BuildNew(); @@ -141,15 +141,15 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_delete_album_metadata_files_that_have_albumid_of_zero() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); Db.Insert(artist); var metadataFile = Builder.CreateNew() - .With(m => m.ArtistId = artist.Id) + .With(m => m.AuthorId = artist.Id) .With(m => m.Type = MetadataType.AlbumMetadata) - .With(m => m.AlbumId = 0) + .With(m => m.BookId = 0) .With(m => m.TrackFileId = null) .BuildNew(); @@ -161,15 +161,15 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_delete_album_image_files_that_have_albumid_of_zero() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); Db.Insert(artist); var metadataFile = Builder.CreateNew() - .With(m => m.ArtistId = artist.Id) + .With(m => m.AuthorId = artist.Id) .With(m => m.Type = MetadataType.AlbumImage) - .With(m => m.AlbumId = 0) + .With(m => m.BookId = 0) .With(m => m.TrackFileId = null) .BuildNew(); @@ -181,13 +181,13 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_delete_track_metadata_files_that_have_trackfileid_of_zero() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .BuildNew(); Db.Insert(artist); var metadataFile = Builder.CreateNew() - .With(m => m.ArtistId = artist.Id) + .With(m => m.AuthorId = artist.Id) .With(m => m.Type = MetadataType.TrackMetadata) .With(m => m.TrackFileId = 0) .BuildNew(); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs index bf4ff402b..bbfcb489a 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedPendingReleasesFixture.cs @@ -28,12 +28,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_not_delete_unorphaned_pending_items() { - var artist = Builder.CreateNew().BuildNew(); + var artist = Builder.CreateNew().BuildNew(); Db.Insert(artist); var pendingRelease = Builder.CreateNew() - .With(h => h.ArtistId = artist.Id) + .With(h => h.AuthorId = artist.Id) .With(h => h.ParsedAlbumInfo = new ParsedAlbumInfo()) .With(h => h.Release = new ReleaseInfo()) .BuildNew(); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTrackFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTrackFilesFixture.cs index 021975573..7c9a7bcc9 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTrackFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTrackFilesFixture.cs @@ -11,42 +11,19 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Housekeeping.Housekeepers { [TestFixture] - public class CleanupOrphanedTrackFilesFixture : DbTest + public class CleanupOrphanedTrackFilesFixture : DbTest { [Test] public void should_unlink_orphaned_track_files() { - var trackFile = Builder.CreateNew() + var trackFile = Builder.CreateNew() .With(h => h.Quality = new QualityModel()) - .With(h => h.AlbumId = 1) + .With(h => h.BookId = 1) .BuildNew(); Db.Insert(trackFile); Subject.Clean(); - AllStoredModels[0].AlbumId.Should().Be(0); - } - - [Test] - public void should_not_unlink_unorphaned_track_files() - { - var trackFiles = Builder.CreateListOfSize(2) - .All() - .With(h => h.Quality = new QualityModel()) - .With(h => h.AlbumId = 1) - .BuildListOfNew(); - - Db.InsertMany(trackFiles); - - var track = Builder.CreateNew() - .With(e => e.TrackFileId = trackFiles.First().Id) - .BuildNew(); - - Db.Insert(track); - - Subject.Clean(); - AllStoredModels.Where(x => x.AlbumId == 1).Should().HaveCount(1); - - Db.All().Should().Contain(e => e.TrackFileId == AllStoredModels.First().Id); + AllStoredModels[0].BookId.Should().Be(0); } } } diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTracksFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTracksFixture.cs deleted file mode 100644 index c40e757ab..000000000 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTracksFixture.cs +++ /dev/null @@ -1,43 +0,0 @@ -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Housekeeping.Housekeepers; -using NzbDrone.Core.Music; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Housekeeping.Housekeepers -{ - [TestFixture] - public class CleanupOrphanedTracksFixture : DbTest - { - [Test] - public void should_delete_orphaned_tracks() - { - var track = Builder.CreateNew() - .BuildNew(); - - Db.Insert(track); - Subject.Clean(); - AllStoredModels.Should().BeEmpty(); - } - - [Test] - public void should_not_delete_unorphaned_tracks() - { - var release = Builder.CreateNew() - .BuildNew(); - - Db.Insert(release); - - var tracks = Builder.CreateListOfSize(2) - .TheFirst(1) - .With(e => e.AlbumReleaseId = release.Id) - .BuildListOfNew(); - - Db.InsertMany(tracks); - Subject.Clean(); - AllStoredModels.Should().HaveCount(1); - AllStoredModels.Should().Contain(e => e.AlbumReleaseId == release.Id); - } - } -} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/UpdateCleanTitleForArtistFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/UpdateCleanTitleForArtistFixture.cs index 1ef06450c..f67a7b18a 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/UpdateCleanTitleForArtistFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/UpdateCleanTitleForArtistFixture.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_update_clean_title() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .With(s => s.Name = "Full Name") .With(s => s.CleanName = "unclean") .Build(); @@ -25,13 +25,13 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Subject.Clean(); Mocker.GetMock() - .Verify(v => v.Update(It.Is(s => s.CleanName == "fullname")), Times.Once()); + .Verify(v => v.Update(It.Is(s => s.CleanName == "fullname")), Times.Once()); } [Test] public void should_not_update_unchanged_title() { - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .With(s => s.Name = "Full Name") .With(s => s.CleanName = "fullname") .Build(); @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Subject.Clean(); Mocker.GetMock() - .Verify(v => v.Update(It.Is(s => s.CleanName == "fullname")), Times.Never()); + .Verify(v => v.Update(It.Is(s => s.CleanName == "fullname")), Times.Never()); } } } diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListServiceFixture.cs index e14102c31..b62c9ace6 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListServiceFixture.cs @@ -3,12 +3,12 @@ using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.ImportLists; -using NzbDrone.Core.ImportLists.ReadarrLists; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.ImportListTests { + /* public class ImportListServiceFixture : DbTest { private List _importLists; @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Test.ImportListTests { _importLists = new List(); - _importLists.Add(Mocker.Resolve()); + _importLists.Add(Mocker.Resolve()); Mocker.SetConstant>(_importLists); } @@ -39,5 +39,5 @@ namespace NzbDrone.Core.Test.ImportListTests AllStoredModels.Should().NotContain(c => c.Id == existingImportLists.Id); } - } + }*/ } diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs index c0c486aa4..1449ddd60 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using FizzWare.NBuilder; using Moq; using NUnit.Framework; using NzbDrone.Core.ImportLists; @@ -29,13 +30,22 @@ namespace NzbDrone.Core.Test.ImportListTests .Setup(v => v.Fetch()) .Returns(_importListReports); - Mocker.GetMock() - .Setup(v => v.SearchForNewArtist(It.IsAny())) - .Returns(new List()); + Mocker.GetMock() + .Setup(v => v.SearchForNewAuthor(It.IsAny())) + .Returns(new List()); - Mocker.GetMock() - .Setup(v => v.SearchForNewAlbum(It.IsAny(), It.IsAny())) - .Returns(new List()); + Mocker.GetMock() + .Setup(v => v.SearchForNewBook(It.IsAny(), It.IsAny())) + .Returns(new List()); + + Mocker.GetMock() + .Setup(v => v.SearchByGoodreadsId(It.IsAny())) + .Returns(x => Builder + .CreateListOfSize(1) + .TheFirst(1) + .With(b => b.GoodreadsId = x) + .With(b => b.ForeignBookId = x.ToString()) + .BuildList()); Mocker.GetMock() .Setup(v => v.Get(It.IsAny())) @@ -48,6 +58,14 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock() .Setup(v => v.All()) .Returns(new List()); + + Mocker.GetMock() + .Setup(v => v.AddAlbums(It.IsAny>(), false)) + .Returns, bool>((x, y) => x); + + Mocker.GetMock() + .Setup(v => v.AddArtists(It.IsAny>(), false)) + .Returns, bool>((x, y) => x); } private void WithAlbum() @@ -55,28 +73,28 @@ namespace NzbDrone.Core.Test.ImportListTests _importListReports.First().Album = "Meteora"; } - private void WithArtistId() + private void WithAuthorId() { _importListReports.First().ArtistMusicBrainzId = "f59c5520-5f46-4d2c-b2c4-822eabf53419"; } - private void WithAlbumId() + private void WithBookId() { - _importListReports.First().AlbumMusicBrainzId = "09474d62-17dd-3a4f-98fb-04c65f38a479"; + _importListReports.First().AlbumMusicBrainzId = "101"; } private void WithExistingArtist() { Mocker.GetMock() .Setup(v => v.FindById(_importListReports.First().ArtistMusicBrainzId)) - .Returns(new Artist { ForeignArtistId = _importListReports.First().ArtistMusicBrainzId }); + .Returns(new Author { ForeignAuthorId = _importListReports.First().ArtistMusicBrainzId }); } private void WithExistingAlbum() { Mocker.GetMock() .Setup(v => v.FindById(_importListReports.First().AlbumMusicBrainzId)) - .Returns(new Album { ForeignAlbumId = _importListReports.First().AlbumMusicBrainzId }); + .Returns(new Book { ForeignBookId = _importListReports.First().AlbumMusicBrainzId }); } private void WithExcludedArtist() @@ -100,7 +118,7 @@ namespace NzbDrone.Core.Test.ImportListTests { new ImportListExclusion { - ForeignId = "09474d62-17dd-3a4f-98fb-04c65f38a479" + ForeignId = "101" } }); } @@ -117,18 +135,18 @@ namespace NzbDrone.Core.Test.ImportListTests { Subject.Execute(new ImportListSyncCommand()); - Mocker.GetMock() - .Verify(v => v.SearchForNewArtist(It.IsAny()), Times.Once()); + Mocker.GetMock() + .Verify(v => v.SearchForNewAuthor(It.IsAny()), Times.Once()); } [Test] public void should_not_search_if_artist_title_and_artist_id() { - WithArtistId(); + WithAuthorId(); Subject.Execute(new ImportListSyncCommand()); - Mocker.GetMock() - .Verify(v => v.SearchForNewArtist(It.IsAny()), Times.Never()); + Mocker.GetMock() + .Verify(v => v.SearchForNewAuthor(It.IsAny()), Times.Never()); } [Test] @@ -137,70 +155,70 @@ namespace NzbDrone.Core.Test.ImportListTests WithAlbum(); Subject.Execute(new ImportListSyncCommand()); - Mocker.GetMock() - .Verify(v => v.SearchForNewAlbum(It.IsAny(), It.IsAny()), Times.Once()); + Mocker.GetMock() + .Verify(v => v.SearchForNewBook(It.IsAny(), It.IsAny()), Times.Once()); } [Test] public void should_not_search_if_album_title_and_album_id() { - WithArtistId(); - WithAlbumId(); + WithAuthorId(); + WithBookId(); Subject.Execute(new ImportListSyncCommand()); - Mocker.GetMock() - .Verify(v => v.SearchForNewAlbum(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock() + .Verify(v => v.SearchForNewBook(It.IsAny(), It.IsAny()), Times.Never()); } [Test] public void should_not_search_if_all_info() { - WithArtistId(); + WithAuthorId(); WithAlbum(); - WithAlbumId(); + WithBookId(); Subject.Execute(new ImportListSyncCommand()); - Mocker.GetMock() - .Verify(v => v.SearchForNewArtist(It.IsAny()), Times.Never()); + Mocker.GetMock() + .Verify(v => v.SearchForNewAuthor(It.IsAny()), Times.Never()); - Mocker.GetMock() - .Verify(v => v.SearchForNewAlbum(It.IsAny(), It.IsAny()), Times.Never()); + Mocker.GetMock() + .Verify(v => v.SearchForNewBook(It.IsAny(), It.IsAny()), Times.Never()); } [Test] public void should_not_add_if_existing_artist() { - WithArtistId(); + WithAuthorId(); WithExistingArtist(); Subject.Execute(new ImportListSyncCommand()); Mocker.GetMock() - .Verify(v => v.AddArtists(It.Is>(t => t.Count == 0))); + .Verify(v => v.AddArtists(It.Is>(t => t.Count == 0), false)); } [Test] public void should_not_add_if_existing_album() { - WithAlbumId(); + WithBookId(); WithExistingAlbum(); Subject.Execute(new ImportListSyncCommand()); Mocker.GetMock() - .Verify(v => v.AddArtists(It.Is>(t => t.Count == 0))); + .Verify(v => v.AddArtists(It.Is>(t => t.Count == 0), false)); } [Test] public void should_add_if_existing_artist_but_new_album() { - WithAlbumId(); + WithBookId(); WithExistingArtist(); Subject.Execute(new ImportListSyncCommand()); Mocker.GetMock() - .Verify(v => v.AddAlbums(It.Is>(t => t.Count == 1))); + .Verify(v => v.AddAlbums(It.Is>(t => t.Count == 1), false)); } [TestCase(ImportListMonitorType.None, false)] @@ -208,13 +226,13 @@ namespace NzbDrone.Core.Test.ImportListTests [TestCase(ImportListMonitorType.EntireArtist, true)] public void should_add_if_not_existing_artist(ImportListMonitorType monitor, bool expectedArtistMonitored) { - WithArtistId(); + WithAuthorId(); WithMonitorType(monitor); Subject.Execute(new ImportListSyncCommand()); Mocker.GetMock() - .Verify(v => v.AddArtists(It.Is>(t => t.Count == 1 && t.First().Monitored == expectedArtistMonitored))); + .Verify(v => v.AddArtists(It.Is>(t => t.Count == 1 && t.First().Monitored == expectedArtistMonitored), false)); } [TestCase(ImportListMonitorType.None, false)] @@ -222,50 +240,50 @@ namespace NzbDrone.Core.Test.ImportListTests [TestCase(ImportListMonitorType.EntireArtist, true)] public void should_add_if_not_existing_album(ImportListMonitorType monitor, bool expectedAlbumMonitored) { - WithAlbumId(); + WithBookId(); WithMonitorType(monitor); Subject.Execute(new ImportListSyncCommand()); Mocker.GetMock() - .Verify(v => v.AddAlbums(It.Is>(t => t.Count == 1 && t.First().Monitored == expectedAlbumMonitored))); + .Verify(v => v.AddAlbums(It.Is>(t => t.Count == 1 && t.First().Monitored == expectedAlbumMonitored), false)); } [Test] public void should_not_add_artist_if_excluded_artist() { - WithArtistId(); + WithAuthorId(); WithExcludedArtist(); Subject.Execute(new ImportListSyncCommand()); Mocker.GetMock() - .Verify(v => v.AddArtists(It.Is>(t => t.Count == 0))); + .Verify(v => v.AddArtists(It.Is>(t => t.Count == 0), false)); } [Test] public void should_not_add_album_if_excluded_album() { - WithAlbumId(); + WithBookId(); WithExcludedAlbum(); Subject.Execute(new ImportListSyncCommand()); Mocker.GetMock() - .Verify(v => v.AddAlbums(It.Is>(t => t.Count == 0))); + .Verify(v => v.AddAlbums(It.Is>(t => t.Count == 0), false)); } [Test] public void should_not_add_album_if_excluded_artist() { - WithAlbumId(); - WithArtistId(); + WithBookId(); + WithAuthorId(); WithExcludedArtist(); Subject.Execute(new ImportListSyncCommand()); Mocker.GetMock() - .Verify(v => v.AddAlbums(It.Is>(t => t.Count == 0))); + .Verify(v => v.AddAlbums(It.Is>(t => t.Count == 0), false)); } } } diff --git a/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyFollowedArtistsFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyFollowedArtistsFixture.cs deleted file mode 100644 index d293b4ec4..000000000 --- a/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyFollowedArtistsFixture.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System.Collections.Generic; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.ImportLists.Spotify; -using NzbDrone.Core.Test.Framework; -using SpotifyAPI.Web; -using SpotifyAPI.Web.Models; - -namespace NzbDrone.Core.Test.ImportListTests -{ - [TestFixture] - public class SpotifyFollowedArtistsFixture : CoreTest - { - // placeholder, we don't use real API - private readonly SpotifyWebAPI _api = null; - - [Test] - public void should_not_throw_if_followed_is_null() - { - Mocker.GetMock(). - Setup(x => x.GetFollowedArtists(It.IsAny(), - It.IsAny())) - .Returns(default(FollowedArtists)); - - var result = Subject.Fetch(_api); - - result.Should().BeEmpty(); - } - - [Test] - public void should_not_throw_if_followed_artists_is_null() - { - var followed = new FollowedArtists - { - Artists = null - }; - - Mocker.GetMock(). - Setup(x => x.GetFollowedArtists(It.IsAny(), - It.IsAny())) - .Returns(followed); - - var result = Subject.Fetch(_api); - - result.Should().BeEmpty(); - } - - [Test] - public void should_not_throw_if_followed_artist_items_is_null() - { - var followed = new FollowedArtists - { - Artists = new CursorPaging - { - Items = null - } - }; - - Mocker.GetMock(). - Setup(x => x.GetFollowedArtists(It.IsAny(), - It.IsAny())) - .Returns(followed); - - var result = Subject.Fetch(_api); - - result.Should().BeEmpty(); - Subject.Fetch(_api); - } - - [Test] - public void should_not_throw_if_artist_is_null() - { - var followed = new FollowedArtists - { - Artists = new CursorPaging - { - Items = new List - { - null - } - } - }; - - Mocker.GetMock(). - Setup(x => x.GetFollowedArtists(It.IsAny(), - It.IsAny())) - .Returns(followed); - - var result = Subject.Fetch(_api); - - result.Should().BeEmpty(); - Subject.Fetch(_api); - } - - [Test] - public void should_parse_followed_artist() - { - var followed = new FollowedArtists - { - Artists = new CursorPaging - { - Items = new List - { - new FullArtist - { - Name = "artist" - } - } - } - }; - - Mocker.GetMock(). - Setup(x => x.GetFollowedArtists(It.IsAny(), - It.IsAny())) - .Returns(followed); - - var result = Subject.Fetch(_api); - - result.Should().HaveCount(1); - } - - [Test] - public void should_not_throw_if_get_next_page_returns_null() - { - var followed = new FollowedArtists - { - Artists = new CursorPaging - { - Items = new List - { - new FullArtist - { - Name = "artist" - } - }, - Next = "DummyToMakeHasNextTrue" - } - }; - - Mocker.GetMock(). - Setup(x => x.GetFollowedArtists(It.IsAny(), - It.IsAny())) - .Returns(followed); - - Mocker.GetMock() - .Setup(x => x.GetNextPage(It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(default(FollowedArtists)); - - var result = Subject.Fetch(_api); - - result.Should().HaveCount(1); - - Mocker.GetMock() - .Verify(v => v.GetNextPage(It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Once()); - } - - [TestCase(null)] - [TestCase("")] - public void should_skip_bad_artist_names(string name) - { - var followed = new FollowedArtists - { - Artists = new CursorPaging - { - Items = new List - { - new FullArtist - { - Name = name - } - } - } - }; - - Mocker.GetMock(). - Setup(x => x.GetFollowedArtists(It.IsAny(), - It.IsAny())) - .Returns(followed); - - var result = Subject.Fetch(_api); - - result.Should().BeEmpty(); - } - } -} diff --git a/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyMappingFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyMappingFixture.cs deleted file mode 100644 index 0697fed4e..000000000 --- a/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyMappingFixture.cs +++ /dev/null @@ -1,345 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Cloud; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.ImportLists.Spotify; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.MetadataSource.SkyHook.Resource; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.ImportListTests -{ - [TestFixture] - - // the base import list class is abstract so use the followed artists one - public class SpotifyMappingFixture : CoreTest - { - [SetUp] - public void Setup() - { - Mocker.SetConstant(new ReadarrCloudRequestBuilder()); - Mocker.SetConstant(Mocker.Resolve()); - } - - [Test] - public void map_artist_should_return_name_if_id_null() - { - var data = new SpotifyImportListItemInfo - { - Artist = "Adele" - }; - - Subject.MapArtistItem(data); - - data.Artist.Should().Be("Adele"); - data.ArtistMusicBrainzId.Should().BeNull(); - data.Album.Should().BeNull(); - data.AlbumMusicBrainzId.Should().BeNull(); - } - - [Test] - public void map_artist_should_set_id_0_if_no_match() - { - Mocker.GetMock() - .Setup(x => x.Get(It.IsAny())) - .Returns((x) => new HttpResponse(new HttpResponse(x, new HttpHeader(), new byte[0], HttpStatusCode.NotFound))); - - var data = new SpotifyImportListItemInfo - { - Artist = "Adele", - ArtistSpotifyId = "id" - }; - - Subject.MapArtistItem(data); - data.ArtistMusicBrainzId.Should().Be("0"); - } - - [Test] - public void map_artist_should_not_update_id_if_http_throws() - { - Mocker.GetMock() - .Setup(x => x.Get(It.IsAny())) - .Throws(new Exception("Dummy exception")); - - var data = new SpotifyImportListItemInfo - { - Artist = "Adele", - ArtistSpotifyId = "id" - }; - - Subject.MapArtistItem(data); - data.ArtistMusicBrainzId.Should().BeNull(); - - ExceptionVerification.ExpectedErrors(1); - } - - [Test] - public void map_artist_should_work() - { - UseRealHttp(); - - var data = new SpotifyImportListItemInfo - { - Artist = "Adele", - ArtistSpotifyId = "4dpARuHxo51G3z768sgnrY" - }; - - Subject.MapArtistItem(data); - data.Should().NotBeNull(); - data.Artist.Should().Be("Adele"); - data.ArtistMusicBrainzId.Should().Be("cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493"); - data.Album.Should().BeNull(); - data.AlbumMusicBrainzId.Should().BeNull(); - } - - [Test] - public void map_album_should_return_name_if_uri_null() - { - var data = new SpotifyImportListItemInfo - { - Album = "25", - Artist = "Adele" - }; - - Subject.MapAlbumItem(data); - data.Should().NotBeNull(); - data.Artist.Should().Be("Adele"); - data.ArtistMusicBrainzId.Should().BeNull(); - data.Album.Should().Be("25"); - data.AlbumMusicBrainzId.Should().BeNull(); - } - - [Test] - public void map_album_should_set_id_0_if_no_match() - { - Mocker.GetMock() - .Setup(x => x.Get(It.IsAny())) - .Returns((x) => new HttpResponse(new HttpResponse(x, new HttpHeader(), new byte[0], HttpStatusCode.NotFound))); - - var data = new SpotifyImportListItemInfo - { - Album = "25", - AlbumSpotifyId = "id", - Artist = "Adele" - }; - - Subject.MapAlbumItem(data); - data.AlbumMusicBrainzId.Should().Be("0"); - } - - [Test] - public void map_album_should_not_update_id_if_http_throws() - { - Mocker.GetMock() - .Setup(x => x.Get(It.IsAny())) - .Throws(new Exception("Dummy exception")); - - var data = new SpotifyImportListItemInfo - { - Album = "25", - AlbumSpotifyId = "id", - Artist = "Adele" - }; - - Subject.MapAlbumItem(data); - data.Should().NotBeNull(); - data.Artist.Should().Be("Adele"); - data.ArtistMusicBrainzId.Should().BeNull(); - data.Album.Should().Be("25"); - data.AlbumMusicBrainzId.Should().BeNull(); - - ExceptionVerification.ExpectedErrors(1); - } - - [Test] - public void map_album_should_work() - { - UseRealHttp(); - - var data = new SpotifyImportListItemInfo - { - Album = "25", - AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", - Artist = "Adele" - }; - - Subject.MapAlbumItem(data); - - data.Should().NotBeNull(); - data.Artist.Should().Be("Adele"); - data.Album.Should().Be("25"); - data.AlbumMusicBrainzId.Should().Be("5537624c-3d2f-4f5c-8099-df916082c85c"); - } - - [Test] - public void map_spotify_releases_should_only_map_album_id_for_album() - { - var data = new List - { - new SpotifyImportListItemInfo - { - Album = "25", - AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", - Artist = "Adele", - ArtistSpotifyId = "4dpARuHxo51G3z768sgnrY" - } - }; - - var map = new List - { - new SpotifyMap - { - SpotifyId = "7uwTHXmFa1Ebi5flqBosig", - MusicbrainzId = "5537624c-3d2f-4f5c-8099-df916082c85c" - }, - new SpotifyMap - { - SpotifyId = "4dpARuHxo51G3z768sgnrY", - MusicbrainzId = "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493" - } - }; - - Mocker.GetMock() - .Setup(x => x.Post>(It.IsAny())) - .Returns(r => new HttpResponse>(new HttpResponse(r, new HttpHeader(), map.ToJson()))); - - var result = Subject.MapSpotifyReleases(data); - result[0].AlbumMusicBrainzId.Should().Be("5537624c-3d2f-4f5c-8099-df916082c85c"); - result[0].ArtistMusicBrainzId.Should().BeNull(); - } - - [Test] - public void map_spotify_releases_should_map_artist_id_for_artist() - { - var data = new List - { - new SpotifyImportListItemInfo - { - Artist = "Adele", - ArtistSpotifyId = "4dpARuHxo51G3z768sgnrY" - } - }; - - var map = new List - { - new SpotifyMap - { - SpotifyId = "7uwTHXmFa1Ebi5flqBosig", - MusicbrainzId = "5537624c-3d2f-4f5c-8099-df916082c85c" - }, - new SpotifyMap - { - SpotifyId = "4dpARuHxo51G3z768sgnrY", - MusicbrainzId = "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493" - } - }; - - Mocker.GetMock() - .Setup(x => x.Post>(It.IsAny())) - .Returns(r => new HttpResponse>(new HttpResponse(r, new HttpHeader(), map.ToJson()))); - - var result = Subject.MapSpotifyReleases(data); - result[0].ArtistMusicBrainzId.Should().Be("cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493"); - } - - [Test] - public void map_spotify_releases_should_drop_not_found() - { - var data = new List - { - new SpotifyImportListItemInfo - { - Album = "25", - AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", - Artist = "Adele" - } - }; - - var map = new List - { - new SpotifyMap - { - SpotifyId = "7uwTHXmFa1Ebi5flqBosig", - MusicbrainzId = "0" - } - }; - - Mocker.GetMock() - .Setup(x => x.Post>(It.IsAny())) - .Returns(r => new HttpResponse>(new HttpResponse(r, new HttpHeader(), map.ToJson()))); - - var result = Subject.MapSpotifyReleases(data); - result.Should().BeEmpty(); - } - - [Test] - public void map_spotify_releases_should_catch_exception_from_api() - { - var data = new List - { - new SpotifyImportListItemInfo - { - Album = "25", - AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", - Artist = "Adele" - } - }; - - Mocker.GetMock() - .Setup(x => x.Post>(It.IsAny())) - .Throws(new Exception("Dummy exception")); - - Mocker.GetMock() - .Setup(x => x.Get(It.IsAny())) - .Throws(new Exception("Dummy exception")); - - var result = Subject.MapSpotifyReleases(data); - result.Should().NotBeNull(); - ExceptionVerification.ExpectedErrors(2); - } - - [Test] - public void map_spotify_releases_should_cope_with_duplicate_spotify_ids() - { - var data = new List - { - new SpotifyImportListItemInfo - { - Album = "25", - AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", - Artist = "Adele" - }, - new SpotifyImportListItemInfo - { - Album = "25", - AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", - Artist = "Adele" - } - }; - - var map = new List - { - new SpotifyMap - { - SpotifyId = "7uwTHXmFa1Ebi5flqBosig", - MusicbrainzId = "5537624c-3d2f-4f5c-8099-df916082c85c" - } - }; - - Mocker.GetMock() - .Setup(x => x.Post>(It.IsAny())) - .Returns(r => new HttpResponse>(new HttpResponse(r, new HttpHeader(), map.ToJson()))); - - var result = Subject.MapSpotifyReleases(data); - result.Should().HaveCount(2); - result[0].AlbumMusicBrainzId.Should().Be("5537624c-3d2f-4f5c-8099-df916082c85c"); - result[1].AlbumMusicBrainzId.Should().Be("5537624c-3d2f-4f5c-8099-df916082c85c"); - } - } -} diff --git a/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyPlaylistFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyPlaylistFixture.cs deleted file mode 100644 index a9f9c5bef..000000000 --- a/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyPlaylistFixture.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System.Collections.Generic; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.ImportLists.Spotify; -using NzbDrone.Core.Test.Framework; -using SpotifyAPI.Web; -using SpotifyAPI.Web.Models; - -namespace NzbDrone.Core.Test.ImportListTests -{ - [TestFixture] - public class SpotifyPlaylistFixture : CoreTest - { - // placeholder, we don't use real API - private readonly SpotifyWebAPI _api = null; - - [Test] - public void should_not_throw_if_playlist_tracks_is_null() - { - Mocker.GetMock(). - Setup(x => x.GetPlaylistTracks(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(default(Paging)); - - var result = Subject.Fetch(_api, "playlistid"); - - result.Should().BeEmpty(); - } - - [Test] - public void should_not_throw_if_playlist_tracks_items_is_null() - { - var playlistTracks = new Paging - { - Items = null - }; - - Mocker.GetMock(). - Setup(x => x.GetPlaylistTracks(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(playlistTracks); - - var result = Subject.Fetch(_api, "playlistid"); - - result.Should().BeEmpty(); - } - - [Test] - public void should_not_throw_if_playlist_track_is_null() - { - var playlistTracks = new Paging - { - Items = new List - { - null - } - }; - - Mocker.GetMock(). - Setup(x => x.GetPlaylistTracks(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(playlistTracks); - - var result = Subject.Fetch(_api, "playlistid"); - - result.Should().BeEmpty(); - } - - [Test] - public void should_use_album_artist_when_it_exists() - { - var playlistTracks = new Paging - { - Items = new List - { - new PlaylistTrack - { - Track = new FullTrack - { - Album = new SimpleAlbum - { - Name = "Album", - Artists = new List - { - new SimpleArtist - { - Name = "AlbumArtist" - } - } - }, - Artists = new List - { - new SimpleArtist - { - Name = "TrackArtist" - } - } - } - } - } - }; - - Mocker.GetMock(). - Setup(x => x.GetPlaylistTracks(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(playlistTracks); - - var result = Subject.Fetch(_api, "playlistid"); - - result.Should().HaveCount(1); - result[0].Artist.Should().Be("AlbumArtist"); - } - - [Test] - public void should_fall_back_to_track_artist_if_album_artist_missing() - { - var playlistTracks = new Paging - { - Items = new List - { - new PlaylistTrack - { - Track = new FullTrack - { - Album = new SimpleAlbum - { - Name = "Album", - Artists = new List - { - new SimpleArtist - { - Name = null - } - } - }, - Artists = new List - { - new SimpleArtist - { - Name = "TrackArtist" - } - } - } - } - } - }; - - Mocker.GetMock(). - Setup(x => x.GetPlaylistTracks(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(playlistTracks); - - var result = Subject.Fetch(_api, "playlistid"); - - result.Should().HaveCount(1); - result[0].Artist.Should().Be("TrackArtist"); - } - - [TestCase(null, null, "Album")] - [TestCase("AlbumArtist", null, null)] - [TestCase(null, "TrackArtist", null)] - public void should_skip_bad_artist_or_album_names(string albumArtistName, string trackArtistName, string albumName) - { - var playlistTracks = new Paging - { - Items = new List - { - new PlaylistTrack - { - Track = new FullTrack - { - Album = new SimpleAlbum - { - Name = albumName, - Artists = new List - { - new SimpleArtist - { - Name = albumArtistName - } - } - }, - Artists = new List - { - new SimpleArtist - { - Name = trackArtistName - } - } - } - } - } - }; - - Mocker.GetMock(). - Setup(x => x.GetPlaylistTracks(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(playlistTracks); - - var result = Subject.Fetch(_api, "playlistid"); - - result.Should().BeEmpty(); - } - - [Test] - public void should_not_throw_if_get_next_page_returns_null() - { - var playlistTracks = new Paging - { - Items = new List - { - new PlaylistTrack - { - Track = new FullTrack - { - Album = new SimpleAlbum - { - Name = "Album", - Artists = new List - { - new SimpleArtist - { - Name = null - } - } - }, - Artists = new List - { - new SimpleArtist - { - Name = "TrackArtist" - } - } - } - } - }, - Next = "DummyToMakeHasNextTrue" - }; - - Mocker.GetMock(). - Setup(x => x.GetPlaylistTracks(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(playlistTracks); - - Mocker.GetMock() - .Setup(x => x.GetNextPage(It.IsAny(), - It.IsAny(), - It.IsAny>())) - .Returns(default(Paging)); - - var result = Subject.Fetch(_api, "playlistid"); - - result.Should().HaveCount(1); - - Mocker.GetMock() - .Verify(x => x.GetNextPage(It.IsAny(), - It.IsAny(), - It.IsAny>()), - Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifySavedAlbumsFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifySavedAlbumsFixture.cs deleted file mode 100644 index 4f1371f5d..000000000 --- a/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifySavedAlbumsFixture.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System.Collections.Generic; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.ImportLists.Spotify; -using NzbDrone.Core.Test.Framework; -using SpotifyAPI.Web; -using SpotifyAPI.Web.Models; - -namespace NzbDrone.Core.Test.ImportListTests -{ - [TestFixture] - public class SpotifySavedAlbumsFixture : CoreTest - { - // placeholder, we don't use real API - private readonly SpotifyWebAPI _api = null; - - [Test] - public void should_not_throw_if_saved_albums_is_null() - { - Mocker.GetMock(). - Setup(x => x.GetSavedAlbums(It.IsAny(), - It.IsAny())) - .Returns(default(Paging)); - - var result = Subject.Fetch(_api); - - result.Should().BeEmpty(); - } - - [Test] - public void should_not_throw_if_saved_album_items_is_null() - { - var savedAlbums = new Paging - { - Items = null - }; - - Mocker.GetMock(). - Setup(x => x.GetSavedAlbums(It.IsAny(), - It.IsAny())) - .Returns(savedAlbums); - - var result = Subject.Fetch(_api); - - result.Should().BeEmpty(); - } - - [Test] - public void should_not_throw_if_saved_album_is_null() - { - var savedAlbums = new Paging - { - Items = new List - { - null - } - }; - - Mocker.GetMock(). - Setup(x => x.GetSavedAlbums(It.IsAny(), - It.IsAny())) - .Returns(savedAlbums); - - var result = Subject.Fetch(_api); - - result.Should().BeEmpty(); - } - - [TestCase("Artist", "Album")] - public void should_parse_saved_album(string artistName, string albumName) - { - var savedAlbums = new Paging - { - Items = new List - { - new SavedAlbum - { - Album = new FullAlbum - { - Name = albumName, - Artists = new List - { - new SimpleArtist - { - Name = artistName - } - } - } - } - } - }; - - Mocker.GetMock(). - Setup(x => x.GetSavedAlbums(It.IsAny(), - It.IsAny())) - .Returns(savedAlbums); - - var result = Subject.Fetch(_api); - - result.Should().HaveCount(1); - } - - [Test] - public void should_not_throw_if_get_next_page_returns_null() - { - var savedAlbums = new Paging - { - Items = new List - { - new SavedAlbum - { - Album = new FullAlbum - { - Name = "Album", - Artists = new List - { - new SimpleArtist - { - Name = "Artist" - } - } - } - } - }, - Next = "DummyToMakeHasNextTrue" - }; - - Mocker.GetMock(). - Setup(x => x.GetSavedAlbums(It.IsAny(), - It.IsAny())) - .Returns(savedAlbums); - - Mocker.GetMock() - .Setup(x => x.GetNextPage(It.IsAny(), - It.IsAny(), - It.IsAny>())) - .Returns(default(Paging)); - - var result = Subject.Fetch(_api); - - result.Should().HaveCount(1); - - Mocker.GetMock() - .Verify(x => x.GetNextPage(It.IsAny(), - It.IsAny(), - It.IsAny>()), - Times.Once()); - } - - [TestCase(null, "Album")] - [TestCase("Artist", null)] - [TestCase(null, null)] - public void should_skip_bad_artist_or_album_names(string artistName, string albumName) - { - var savedAlbums = new Paging - { - Items = new List - { - new SavedAlbum - { - Album = new FullAlbum - { - Name = albumName, - Artists = new List - { - new SimpleArtist - { - Name = artistName - } - } - } - } - } - }; - - Mocker.GetMock(). - Setup(x => x.GetSavedAlbums(It.IsAny(), - It.IsAny())) - .Returns(savedAlbums); - - var result = Subject.Fetch(_api); - - result.Should().BeEmpty(); - } - } -} diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs index bcad5015a..c9ad90daa 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs @@ -14,12 +14,12 @@ namespace NzbDrone.Core.Test.IndexerSearchTests [TestFixture] public class ArtistSearchServiceFixture : CoreTest { - private Artist _artist; + private Author _artist; [SetUp] public void Setup() { - _artist = new Artist(); + _artist = new Author(); Mocker.GetMock() .Setup(s => s.GetArtist(It.IsAny())) @@ -37,17 +37,17 @@ namespace NzbDrone.Core.Test.IndexerSearchTests [Test] public void should_only_include_monitored_albums() { - _artist.Albums = new List + _artist.Books = new List { - new Album { Monitored = false }, - new Album { Monitored = true } + new Book { Monitored = false }, + new Book { Monitored = true } }; - Subject.Execute(new ArtistSearchCommand { ArtistId = _artist.Id, Trigger = CommandTrigger.Manual }); + Subject.Execute(new ArtistSearchCommand { AuthorId = _artist.Id, Trigger = CommandTrigger.Manual }); Mocker.GetMock() .Verify(v => v.ArtistSearch(_artist.Id, false, true, false), - Times.Exactly(_artist.Albums.Value.Count(s => s.Monitored))); + Times.Exactly(_artist.Books.Value.Count(s => s.Monitored))); } } } diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs index a0a921ce8..e429cc08e 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests [TestCase("방탄소년단", "방탄소년단")] public void should_replace_some_special_characters_artist(string artist, string expected) { - Subject.Artist = new Artist { Name = artist }; + Subject.Artist = new Author { Name = artist }; Subject.ArtistQuery.Should().Be(expected); } @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests [TestCase("Sad Clowns & Hillbillies", "Sad+Clowns+Hillbillies")] [TestCase("¿Quién sabe?", "Quien+sabe")] [TestCase("Seal the Deal & Let’s Boogie", "Seal+the+Deal+Lets+Boogie")] - [TestCase("Section.80", "Section80")] + [TestCase("Section.80", "Section+80")] public void should_replace_some_special_characters(string album, string expected) { Subject.AlbumTitle = album; diff --git a/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesCapabilitiesProviderFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesCapabilitiesProviderFixture.cs deleted file mode 100644 index ec9567d34..000000000 --- a/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesCapabilitiesProviderFixture.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Xml; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Http; -using NzbDrone.Core.Indexers.Headphones; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.IndexerTests.HeadphonesTests -{ - [TestFixture] - public class HeadphonesCapabilitiesProviderFixture : CoreTest - { - private HeadphonesSettings _settings; - private string _caps; - - [SetUp] - public void SetUp() - { - _settings = new HeadphonesSettings(); - - _caps = ReadAllText("Files/Indexers/Newznab/newznab_caps.xml"); - } - - private void GivenCapsResponse(string caps) - { - Mocker.GetMock() - .Setup(o => o.Get(It.IsAny())) - .Returns(r => new HttpResponse(r, new HttpHeader(), caps)); - } - - [Test] - public void should_not_request_same_caps_twice() - { - GivenCapsResponse(_caps); - - Subject.GetCapabilities(_settings); - Subject.GetCapabilities(_settings); - - Mocker.GetMock() - .Verify(o => o.Get(It.IsAny()), Times.Once()); - } - - [Test] - public void should_report_pagesize() - { - GivenCapsResponse(_caps); - - var caps = Subject.GetCapabilities(_settings); - - caps.DefaultPageSize.Should().Be(25); - caps.MaxPageSize.Should().Be(60); - } - - [Test] - public void should_use_default_pagesize_if_missing() - { - GivenCapsResponse(_caps.Replace("() - .Setup(o => o.Get(It.IsAny())) - .Throws(); - - Assert.Throws(() => Subject.GetCapabilities(_settings)); - } - - [Test] - public void should_throw_if_xml_invalid() - { - GivenCapsResponse(_caps.Replace("")); - - Assert.Throws(() => Subject.GetCapabilities(_settings)); - } - - [Test] - public void should_not_throw_on_xml_data_unexpected() - { - GivenCapsResponse(_caps.Replace("3040", "asdf")); - - var result = Subject.GetCapabilities(_settings); - - result.Should().NotBeNull(); - - ExceptionVerification.ExpectedErrors(1); - } - } -} diff --git a/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesFixture.cs deleted file mode 100644 index 88678f923..000000000 --- a/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesFixture.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Linq; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Http; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.Headphones; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.IndexerTests.HeadphonesTests -{ - [TestFixture] - public class HeadphonesFixture : CoreTest - { - private HeadphonesCapabilities _caps; - - [SetUp] - public void Setup() - { - Subject.Definition = new IndexerDefinition() - { - Name = "Headphones VIP", - Settings = new HeadphonesSettings() - { - Categories = new int[] { 3000 }, - Username = "user", - Password = "pass" - } - }; - - _caps = new HeadphonesCapabilities(); - Mocker.GetMock() - .Setup(v => v.GetCapabilities(It.IsAny())) - .Returns(_caps); - } - - [Test] - public void should_parse_recent_feed_from_headphones() - { - var recentFeed = ReadAllText(@"Files/Indexers/Headphones/Headphones.xml"); - - Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) - .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); - - var releases = Subject.FetchRecent(); - - releases.Should().HaveCount(16); - - releases.First().Should().BeOfType(); - var releaseInfo = releases.First() as ReleaseInfo; - - releaseInfo.Title.Should().Be("Lady Gaga Born This Way 2CD FLAC 2011 WRE"); - releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet); - releaseInfo.DownloadUrl.Should().Be("https://indexer.codeshy.com/api?t=g&guid=123456&apikey=123456789"); - releaseInfo.BasicAuthString.Should().Be("dXNlcjpwYXNz"); - releaseInfo.Indexer.Should().Be(Subject.Definition.Name); - releaseInfo.PublishDate.Should().Be(DateTime.Parse("2013/06/02 08:58:54")); - releaseInfo.Size.Should().Be(917347414); - } - - [Test] - public void should_use_pagesize_reported_by_caps() - { - _caps.MaxPageSize = 30; - _caps.DefaultPageSize = 25; - - Subject.PageSize.Should().Be(25); - } - } -} diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs index 39bd839a0..d13a6972e 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests _singleAlbumSearchCriteria = new AlbumSearchCriteria { - Artist = new Music.Artist { Name = "Alien Ant Farm" }, + Artist = new Music.Author { Name = "Alien Ant Farm" }, AlbumTitle = "TruANT" }; diff --git a/src/NzbDrone.Core.Test/IndexerTests/WafflesTests/WafflesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/WafflesTests/WafflesFixture.cs deleted file mode 100644 index eb313b1ef..000000000 --- a/src/NzbDrone.Core.Test/IndexerTests/WafflesTests/WafflesFixture.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Linq; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Http; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.Waffles; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.IndexerTests.WafflesTests -{ - [TestFixture] - public class WafflesFixture : CoreTest - { - [SetUp] - public void Setup() - { - Subject.Definition = new IndexerDefinition() - { - Name = "Waffles", - Settings = new WafflesSettings() - { - UserId = "xxx", - RssPasskey = "123456789" - } - }; - } - - [Test] - public void should_parse_recent_feed_from_waffles() - { - var recentFeed = ReadAllText(@"Files/Indexers/Waffles/waffles.xml"); - - Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) - .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); - - var releases = Subject.FetchRecent(); - - releases.Should().HaveCount(15); - - var releaseInfo = releases.First(); - - releaseInfo.Title.Should().Be("Coldplay - Kaleidoscope EP (FLAC HD) [2017-Web-FLAC-Lossless]"); - releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); - releaseInfo.DownloadUrl.Should().Be("https://waffles.ch/download.php/xxx/1166992/" + - "Coldplay%20-%20Kaleidoscope%20EP%20%28FLAC%20HD%29%20%5B2017-Web-FLAC-Lossless%5D.torrent?passkey=123456789&uid=xxx&rss=1"); - releaseInfo.InfoUrl.Should().Be("https://waffles.ch/details.php?id=1166992&hit=1"); - releaseInfo.CommentUrl.Should().Be("https://waffles.ch/details.php?id=1166992&hit=1"); - releaseInfo.Indexer.Should().Be(Subject.Definition.Name); - releaseInfo.PublishDate.Should().Be(DateTime.Parse("2017-07-16 09:51:54")); - releaseInfo.Size.Should().Be(552668227); - } - } -} diff --git a/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs b/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs index f08b3e6b2..2f19d1dd8 100644 --- a/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs +++ b/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs @@ -100,7 +100,7 @@ namespace NzbDrone.Core.Test.Instrumentation [Test] public void null_string_as_arg_should_not_fail() { - var epFile = new TrackFile(); + var epFile = new BookFile(); _logger.Debug("File {0} no longer exists on disk. removing from database.", epFile.Path); Thread.Sleep(1000); diff --git a/src/NzbDrone.Core.Test/MediaCoverTests/MediaCoverServiceFixture.cs b/src/NzbDrone.Core.Test/MediaCoverTests/MediaCoverServiceFixture.cs index 38ffd8436..a4f7da7be 100644 --- a/src/NzbDrone.Core.Test/MediaCoverTests/MediaCoverServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaCoverTests/MediaCoverServiceFixture.cs @@ -18,8 +18,8 @@ namespace NzbDrone.Core.Test.MediaCoverTests [TestFixture] public class MediaCoverServiceFixture : CoreTest { - private Artist _artist; - private Album _album; + private Author _artist; + private Book _album; private HttpResponse _httpResponse; [SetUp] @@ -27,12 +27,12 @@ namespace NzbDrone.Core.Test.MediaCoverTests { Mocker.SetConstant(new AppFolderInfo(Mocker.Resolve())); - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(v => v.Id = 2) .With(v => v.Metadata.Value.Images = new List { new MediaCover.MediaCover(MediaCoverTypes.Poster, "") }) .Build(); - _album = Builder.CreateNew() + _album = Builder.CreateNew() .With(v => v.Id = 4) .With(v => v.Images = new List { new MediaCover.MediaCover(MediaCoverTypes.Cover, "") }) .Build(); @@ -140,7 +140,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests Mocker.GetMock() .Setup(v => v.GetAlbumsByArtist(It.IsAny())) - .Returns(new List { _album }); + .Returns(new List { _album }); Mocker.GetMock() .Setup(v => v.FileExists(It.IsAny())) @@ -161,7 +161,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests Mocker.GetMock() .Setup(v => v.GetAlbumsByArtist(It.IsAny())) - .Returns(new List { _album }); + .Returns(new List { _album }); Mocker.GetMock() .Setup(v => v.FileExists(It.IsAny())) @@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests Mocker.GetMock() .Setup(v => v.GetAlbumsByArtist(It.IsAny())) - .Returns(new List { _album }); + .Returns(new List { _album }); Mocker.GetMock() .Setup(v => v.GetFileSize(It.IsAny())) @@ -211,7 +211,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests Mocker.GetMock() .Setup(v => v.GetAlbumsByArtist(It.IsAny())) - .Returns(new List { _album }); + .Returns(new List { _album }); Mocker.GetMock() .Setup(v => v.GetFileSize(It.IsAny())) @@ -236,7 +236,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests Mocker.GetMock() .Setup(v => v.GetAlbumsByArtist(It.IsAny())) - .Returns(new List { _album }); + .Returns(new List { _album }); Mocker.GetMock() .Setup(v => v.Resize(It.IsAny(), It.IsAny(), It.IsAny())) diff --git a/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs index b461298f4..af65be391 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs @@ -17,6 +17,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture { [TestFixture] + [Ignore("Readarr doesn't currently support audio")] public class AudioTagServiceFixture : CoreTest { public static class TestCaseFactory @@ -195,40 +196,6 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture VerifySame(writtentags, _testTags, skipProperties); } - [Test] - [TestCaseSource(typeof(TestCaseFactory), "TestCases")] - public void should_remove_mb_tags(string filename, string[] skipProperties) - { - GivenFileCopy(filename); - var path = _copiedFile; - - var track = new TrackFile - { - Path = path - }; - - _testTags.Write(path); - - var withmb = Subject.ReadAudioTag(path); - - VerifySame(withmb, _testTags, skipProperties); - - Subject.RemoveMusicBrainzTags(track); - - var tag = Subject.ReadAudioTag(path); - - tag.MusicBrainzReleaseCountry.Should().BeNull(); - tag.MusicBrainzReleaseStatus.Should().BeNull(); - tag.MusicBrainzReleaseType.Should().BeNull(); - tag.MusicBrainzReleaseId.Should().BeNull(); - tag.MusicBrainzArtistId.Should().BeNull(); - tag.MusicBrainzReleaseArtistId.Should().BeNull(); - tag.MusicBrainzReleaseGroupId.Should().BeNull(); - tag.MusicBrainzTrackId.Should().BeNull(); - tag.MusicBrainzAlbumComment.Should().BeNull(); - tag.MusicBrainzReleaseTrackId.Should().BeNull(); - } - [Test] [TestCaseSource(typeof(TestCaseFactory), "TestCases")] public void should_read_audiotag_from_file_with_no_tags(string filename, string[] skipProperties) @@ -337,40 +304,19 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture tag.OriginalReleaseDate.HasValue.Should().BeFalse(); } - private TrackFile GivenPopulatedTrackfile(int mediumOffset) + private BookFile GivenPopulatedTrackfile(int mediumOffset) { - var meta = Builder.CreateNew().Build(); - var artist = Builder.CreateNew() + var meta = Builder.CreateNew().Build(); + var artist = Builder.CreateNew() .With(x => x.Metadata = meta) .Build(); - var album = Builder.CreateNew() - .With(x => x.Artist = artist) + var album = Builder.CreateNew() + .With(x => x.Author = artist) .Build(); - var media = Builder.CreateListOfSize(2).Build() as List; - media.ForEach(x => x.Number += mediumOffset); - - var release = Builder.CreateNew() + var file = Builder.CreateNew() .With(x => x.Album = album) - .With(x => x.Media = media) - .With(x => x.Country = new List()) - .With(x => x.Label = new List()) - .Build(); - - var tracks = Builder.CreateListOfSize(10) - .All() - .With(x => x.AlbumRelease = release) - .With(x => x.ArtistMetadata = meta) - .TheFirst(5) - .With(x => x.MediumNumber = 1 + mediumOffset) - .TheNext(5) - .With(x => x.MediumNumber = 2 + mediumOffset) - .Build() as List; - release.Tracks = tracks; - - var file = Builder.CreateNew() - .With(x => x.Tracks = new List { tracks[0] }) .With(x => x.Artist = artist) .Build(); @@ -382,8 +328,6 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture { var file = GivenPopulatedTrackfile(0); var tag = Subject.GetTrackMetadata(file); - - tag.MusicBrainzReleaseCountry.Should().BeNull(); } [Test] diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs index 70fecacbc..44036d1eb 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs @@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests [TestFixture] public class ScanFixture : FileSystemTest { - private Artist _artist; + private Author _artist; private string _rootFolder; private string _otherArtistFolder; @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests _otherArtistFolder = @"C:\Test\Music\OtherArtist".AsOsAgnostic(); var artistFolder = @"C:\Test\Music\Artist".AsOsAgnostic(); - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(s => s.Path = artistFolder) .Build(); @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Mocker.GetMock() .Setup(s => s.GetArtists(It.IsAny>())) - .Returns(new List()); + .Returns(new List()); Mocker.GetMock() .Setup(v => v.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -53,11 +53,11 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Mocker.GetMock() .Setup(v => v.GetFilesByArtist(It.IsAny())) - .Returns(new List()); + .Returns(new List()); Mocker.GetMock() .Setup(v => v.GetFilesWithBasePath(It.IsAny())) - .Returns(new List()); + .Returns(new List()); Mocker.GetMock() .Setup(v => v.FilterUnchangedFiles(It.IsAny>(), It.IsAny())) @@ -105,7 +105,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Mocker.GetMock() .Setup(x => x.GetFilesWithBasePath(_artist.Path)) - .Returns(files.Select(x => new TrackFile + .Returns(files.Select(x => new BookFile { Path = x, Modified = lastWrite.Value.UtcDateTime @@ -168,8 +168,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "file1.flac"), - Path.Combine(_artist.Path, "s01e01.flac") + Path.Combine(_artist.Path, "file1.mobi"), + Path.Combine(_artist.Path, "s01e01.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -185,11 +185,11 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "EXTRAS", "file1.flac"), - Path.Combine(_artist.Path, "Extras", "file2.flac"), - Path.Combine(_artist.Path, "EXTRAs", "file3.flac"), - Path.Combine(_artist.Path, "ExTrAs", "file4.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, "EXTRAS", "file1.mobi"), + Path.Combine(_artist.Path, "Extras", "file2.mobi"), + Path.Combine(_artist.Path, "EXTRAs", "file3.mobi"), + Path.Combine(_artist.Path, "ExTrAs", "file4.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -205,9 +205,9 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, ".AppleDouble", "file1.flac"), - Path.Combine(_artist.Path, ".appledouble", "file2.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, ".AppleDouble", "file1.mobi"), + Path.Combine(_artist.Path, ".appledouble", "file2.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -225,12 +225,12 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "Extras", "file1.flac"), - Path.Combine(_artist.Path, ".AppleDouble", "file2.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e02.flac"), - Path.Combine(_artist.Path, "Season 2", "s02e01.flac"), - Path.Combine(_artist.Path, "Season 2", "s02e02.flac"), + Path.Combine(_artist.Path, "Extras", "file1.mobi"), + Path.Combine(_artist.Path, ".AppleDouble", "file2.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e02.mobi"), + Path.Combine(_artist.Path, "Season 2", "s02e01.mobi"), + Path.Combine(_artist.Path, "Season 2", "s02e02.mobi"), }); Subject.Scan(new List { _artist.Path }); @@ -246,7 +246,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "Album 1", ".t01.mp3") + Path.Combine(_artist.Path, "Album 1", ".t01.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -262,10 +262,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, ".@__thumb", "file1.flac"), - Path.Combine(_artist.Path, ".@__THUMB", "file2.flac"), - Path.Combine(_artist.Path, ".hidden", "file2.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, ".@__thumb", "file1.mobi"), + Path.Combine(_artist.Path, ".@__THUMB", "file2.mobi"), + Path.Combine(_artist.Path, ".hidden", "file2.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -281,11 +281,11 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "Season 1", ".@__thumb", "file1.flac"), - Path.Combine(_artist.Path, "Season 1", ".@__THUMB", "file2.flac"), - Path.Combine(_artist.Path, "Season 1", ".hidden", "file2.flac"), - Path.Combine(_artist.Path, "Season 1", ".AppleDouble", "s01e01.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, "Season 1", ".@__thumb", "file1.mobi"), + Path.Combine(_artist.Path, "Season 1", ".@__THUMB", "file2.mobi"), + Path.Combine(_artist.Path, "Season 1", ".hidden", "file2.mobi"), + Path.Combine(_artist.Path, "Season 1", ".AppleDouble", "s01e01.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -301,8 +301,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "@eaDir", "file1.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, "@eaDir", "file1.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -318,8 +318,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, ".@__thumb", "file1.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, ".@__thumb", "file1.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -337,8 +337,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "Season 1", "file1.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, "Season 1", "file1.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -355,8 +355,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { Path.Combine(_artist.Path, ".DS_STORE"), - Path.Combine(_artist.Path, "._24 The Status Quo Combustion.flac"), - Path.Combine(_artist.Path, "24 The Status Quo Combustion.flac") + Path.Combine(_artist.Path, "._24 The Status Quo Combustion.mobi"), + Path.Combine(_artist.Path, "24 The Status Quo Combustion.mobi") }); Subject.Scan(new List { _artist.Path }); @@ -386,8 +386,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests { var files = new List { - Path.Combine(_artist.Path, "Season 1", "file1.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, "Season 1", "file1.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }; GivenFiles(files); @@ -397,7 +397,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(new List { _artist.Path }); Mocker.GetMock() - .Verify(x => x.AddMany(It.Is>(l => l.Select(t => t.Path).SequenceEqual(files))), + .Verify(x => x.AddMany(It.Is>(l => l.Select(t => t.Path).SequenceEqual(files))), Times.Once()); } @@ -406,8 +406,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests { var files = new List { - Path.Combine(_artist.Path, "Season 1", "file1.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, "Season 1", "file1.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }; GivenFiles(files); @@ -417,7 +417,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(new List { _artist.Path }); Mocker.GetMock() - .Verify(x => x.AddMany(It.Is>(l => l.Select(t => t.Path).SequenceEqual(files.GetRange(0, 1)))), + .Verify(x => x.AddMany(It.Is>(l => l.Select(t => t.Path).SequenceEqual(files.GetRange(0, 1)))), Times.Once()); } @@ -426,8 +426,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests { var files = new List { - Path.Combine(_artist.Path, "Season 1", "file1.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, "Season 1", "file1.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }; GivenFiles(files); @@ -437,11 +437,11 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(new List { _artist.Path }); Mocker.GetMock() - .Verify(x => x.AddMany(It.Is>(l => l.Count == 0)), + .Verify(x => x.AddMany(It.Is>(l => l.Count == 0)), Times.Once()); Mocker.GetMock() - .Verify(x => x.AddMany(It.Is>(l => l.Count > 0)), + .Verify(x => x.AddMany(It.Is>(l => l.Count > 0)), Times.Never()); } @@ -450,8 +450,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests { var files = new List { - Path.Combine(_artist.Path, "Season 1", "file1.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, "Season 1", "file1.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }; GivenFiles(files); @@ -461,11 +461,11 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(new List { _artist.Path }); Mocker.GetMock() - .Verify(x => x.Update(It.Is>(l => l.Count == 0)), + .Verify(x => x.Update(It.Is>(l => l.Count == 0)), Times.Once()); Mocker.GetMock() - .Verify(x => x.Update(It.Is>(l => l.Count > 0)), + .Verify(x => x.Update(It.Is>(l => l.Count > 0)), Times.Never()); } @@ -474,8 +474,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests { var files = new List { - Path.Combine(_artist.Path, "Season 1", "file1.flac"), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + Path.Combine(_artist.Path, "Season 1", "file1.mobi"), + Path.Combine(_artist.Path, "Season 1", "s01e01.mobi") }; GivenFiles(files, new DateTime(2019, 2, 1)); @@ -485,7 +485,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(new List { _artist.Path }); Mocker.GetMock() - .Verify(x => x.Update(It.Is>(l => l.Count == 2)), + .Verify(x => x.Update(It.Is>(l => l.Count == 2)), Times.Once()); } @@ -494,7 +494,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests { var files = new List { - Path.Combine(_artist.Path, "Season 1", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", "file1.mobi"), }; GivenKnownFiles(files); @@ -505,7 +505,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests .With(x => x.Path = files[0]) .With(x => x.Modified = new DateTime(2019, 2, 1)) .With(x => x.Size = 100) - .With(x => x.Quality = new QualityModel(Quality.FLAC)) + .With(x => x.Quality = new QualityModel(Quality.MOBI)) .With(x => x.FileTrackInfo = new ParsedTrackInfo { MediaInfo = Builder.CreateNew().Build() @@ -519,7 +519,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(new List { _artist.Path }); Mocker.GetMock() - .Verify(x => x.Update(It.Is>( + .Verify(x => x.Update(It.Is>( l => l.Count == 1 && l[0].Path == localTrack.Path && l[0].Modified == localTrack.Modified && diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs index 9fac7f97a..21046f9eb 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(new List()); Mocker.GetMock() - .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessPath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List()); var downloadItem = Builder.CreateNew() @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Build(); var remoteAlbum = Builder.CreateNew() - .With(v => v.Artist = new Artist()) + .With(v => v.Artist = new Author()) .Build(); _trackedDownload = new TrackedDownload diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs index 113f2b32f..b06834749 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs @@ -50,7 +50,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Build(); var remoteAlbum = Builder.CreateNew() - .With(v => v.Artist = new Artist()) + .With(v => v.Artist = new Author()) .Build(); _trackedDownload = new TrackedDownload @@ -73,7 +73,7 @@ namespace NzbDrone.Core.Test.MediaFiles { Mocker.GetMock() .Setup(s => s.GetArtist(It.IsAny())) - .Returns(Builder.CreateNew().Build()); + .Returns(Builder.CreateNew().Build()); } private void GivenSuccessfulImport() @@ -125,7 +125,7 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_skip_if_no_artist_found() { - Mocker.GetMock().Setup(c => c.GetArtist("foldername")).Returns((Artist)null); + Mocker.GetMock().Setup(c => c.GetArtist("foldername")).Returns((Author)null); Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedTracksFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedTracksFixture.cs index 2dc8eb36c..0638ebe62 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedTracksFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedTracksFixture.cs @@ -34,56 +34,41 @@ namespace NzbDrone.Core.Test.MediaFiles _rejectedDecisions = new List>(); _approvedDecisions = new List>(); - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .With(s => s.Path = @"C:\Test\Music\Alien Ant Farm".AsOsAgnostic()) .Build(); - var album = Builder.CreateNew() - .With(e => e.Artist = artist) + var album = Builder.CreateNew() + .With(e => e.Author = artist) .Build(); - var release = Builder.CreateNew() - .With(e => e.AlbumId = album.Id) - .With(e => e.Monitored = true) - .Build(); - - album.AlbumReleases = new List { release }; - - var tracks = Builder.CreateListOfSize(5) - .Build(); - _rejectedDecisions.Add(new ImportDecision(new LocalTrack(), new Rejection("Rejected!"))); _rejectedDecisions.Add(new ImportDecision(new LocalTrack(), new Rejection("Rejected!"))); _rejectedDecisions.Add(new ImportDecision(new LocalTrack(), new Rejection("Rejected!"))); - foreach (var track in tracks) - { - _approvedDecisions.Add(new ImportDecision( - new LocalTrack + _approvedDecisions.Add(new ImportDecision( + new LocalTrack + { + Artist = artist, + Album = album, + Path = Path.Combine(artist.Path, "Alien Ant Farm - 01 - Pilot.mp3"), + Quality = new QualityModel(Quality.MP3_320), + FileTrackInfo = new ParsedTrackInfo { - Artist = artist, - Album = album, - Release = release, - Tracks = new List { track }, - Path = Path.Combine(artist.Path, "Alien Ant Farm - 01 - Pilot.mp3"), - Quality = new QualityModel(Quality.MP3_256), - FileTrackInfo = new ParsedTrackInfo - { - ReleaseGroup = "DRONE" - } - })); - } + ReleaseGroup = "DRONE" + } + })); Mocker.GetMock() - .Setup(s => s.UpgradeTrackFile(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.UpgradeTrackFile(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new TrackFileMoveResult()); _downloadClientItem = Builder.CreateNew().Build(); Mocker.GetMock() .Setup(s => s.GetFilesByAlbum(It.IsAny())) - .Returns(new List()); + .Returns(new List()); } [Test] @@ -91,13 +76,13 @@ namespace NzbDrone.Core.Test.MediaFiles { Subject.Import(_rejectedDecisions, false).Where(i => i.Result == ImportResultType.Imported).Should().BeEmpty(); - Mocker.GetMock().Verify(v => v.Add(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(v => v.Add(It.IsAny()), Times.Never()); } [Test] public void should_import_each_approved() { - Subject.Import(_approvedDecisions, false).Should().HaveCount(5); + Subject.Import(_approvedDecisions, false).Should().HaveCount(1); } [Test] @@ -131,7 +116,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Import(new List> { _approvedDecisions.First() }, true); Mocker.GetMock() - .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().Item, false), + .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().Item, false), Times.Once()); } @@ -152,7 +137,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Import(new List> { track }, false); Mocker.GetMock() - .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().Item, false), + .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().Item, false), Times.Never()); } @@ -167,9 +152,8 @@ namespace NzbDrone.Core.Test.MediaFiles { Artist = fileDecision.Item.Artist, Album = fileDecision.Item.Album, - Tracks = new List { fileDecision.Item.Tracks.First() }, Path = @"C:\Test\Music\Alien Ant Farm\Alien Ant Farm - 01 - Pilot.mp3".AsOsAgnostic(), - Quality = new QualityModel(Quality.MP3_256), + Quality = new QualityModel(Quality.MP3_320), Size = 80.Megabytes() }); @@ -190,7 +174,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Import(new List> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false }); Mocker.GetMock() - .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().Item, true), Times.Once()); + .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().Item, true), Times.Once()); } [Test] @@ -199,7 +183,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Import(new List> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false }, ImportMode.Move); Mocker.GetMock() - .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().Item, false), Times.Once()); + .Verify(v => v.UpgradeTrackFile(It.IsAny(), _approvedDecisions.First().Item, false), Times.Once()); } [Test] @@ -207,14 +191,14 @@ namespace NzbDrone.Core.Test.MediaFiles { Mocker.GetMock() .Setup(s => s.GetFileWithPath(It.IsAny())) - .Returns(Builder.CreateNew().Build()); + .Returns(Builder.CreateNew().Build()); var track = _approvedDecisions.First(); track.Item.ExistingFile = true; Subject.Import(new List> { track }, false); Mocker.GetMock() - .Verify(v => v.Delete(It.IsAny(), DeleteMediaFileReason.ManualOverride), Times.Once()); + .Verify(v => v.Delete(It.IsAny(), DeleteMediaFileReason.ManualOverride), Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs index 48d3006db..30d099648 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs @@ -15,17 +15,17 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileDeletionService public class DeleteTrackFileFixture : CoreTest { private static readonly string RootFolder = @"C:\Test\Music"; - private Artist _artist; - private TrackFile _trackFile; + private Author _artist; + private BookFile _trackFile; [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(s => s.Path = Path.Combine(RootFolder, "Artist Name")) .Build(); - _trackFile = Builder.CreateNew() + _trackFile = Builder.CreateNew() .With(f => f.Path = "/Artist Name - Track01") .Build(); diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs index bea7f05ba..c627b90aa 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs @@ -12,76 +12,43 @@ using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.MediaFiles { [TestFixture] - public class MediaFileRepositoryFixture : DbTest + public class MediaFileRepositoryFixture : DbTest { - private Artist _artist; - private Album _album; - private List _releases; + private Author _artist; + private Book _album; [SetUp] public void Setup() { - var meta = Builder.CreateNew() + var meta = Builder.CreateNew() .With(a => a.Id = 0) .Build(); Db.Insert(meta); - _artist = Builder.CreateNew() - .With(a => a.ArtistMetadataId = meta.Id) + _artist = Builder.CreateNew() + .With(a => a.AuthorMetadataId = meta.Id) .With(a => a.Id = 0) .Build(); Db.Insert(_artist); - _album = Builder.CreateNew() + _album = Builder.CreateNew() .With(a => a.Id = 0) - .With(a => a.ArtistMetadataId = _artist.ArtistMetadataId) + .With(a => a.AuthorMetadataId = _artist.AuthorMetadataId) .Build(); Db.Insert(_album); - _releases = Builder.CreateListOfSize(2) - .All() - .With(a => a.Id = 0) - .With(a => a.AlbumId = _album.Id) - .TheFirst(1) - .With(a => a.Monitored = true) - .TheNext(1) - .With(a => a.Monitored = false) - .Build().ToList(); - Db.InsertMany(_releases); - - var files = Builder.CreateListOfSize(10) + var files = Builder.CreateListOfSize(10) .All() .With(c => c.Id = 0) - .With(c => c.Quality = new QualityModel(Quality.MP3_192)) + .With(c => c.Quality = new QualityModel(Quality.MP3_320)) .TheFirst(5) - .With(c => c.AlbumId = _album.Id) + .With(c => c.BookId = _album.Id) .TheFirst(1) .With(c => c.Path = @"C:\Test\Path\Artist\somefile1.flac".AsOsAgnostic()) .TheNext(1) .With(c => c.Path = @"C:\Test\Path\Artist\somefile2.flac".AsOsAgnostic()) .BuildListOfNew(); Db.InsertMany(files); - - var track = Builder.CreateListOfSize(10) - .All() - .With(a => a.Id = 0) - .TheFirst(4) - .With(a => a.AlbumReleaseId = _releases[0].Id) - .TheFirst(1) - .With(a => a.TrackFileId = files[0].Id) - .TheNext(1) - .With(a => a.TrackFileId = files[1].Id) - .TheNext(1) - .With(a => a.TrackFileId = files[2].Id) - .TheNext(1) - .With(a => a.TrackFileId = files[3].Id) - .TheNext(1) - .With(a => a.TrackFileId = files[4].Id) - .With(a => a.AlbumReleaseId = _releases[1].Id) - .TheNext(5) - .With(a => a.TrackFileId = 0) - .Build(); - Db.InsertMany(track); } [Test] @@ -104,19 +71,6 @@ namespace NzbDrone.Core.Test.MediaFiles unmappedfiles.Should().HaveCount(5); } - [Test] - public void get_files_by_release() - { - VerifyData(); - var firstReleaseFiles = Subject.GetFilesByRelease(_releases[0].Id); - var secondReleaseFiles = Subject.GetFilesByRelease(_releases[1].Id); - VerifyEagerLoaded(firstReleaseFiles); - VerifyEagerLoaded(secondReleaseFiles); - - firstReleaseFiles.Should().HaveCount(4); - secondReleaseFiles.Should().HaveCount(1); - } - [TestCase("C:\\Test\\Path")] [TestCase("C:\\Test\\Path\\")] public void get_files_by_base_path_should_cope_with_trailing_slash(string dir) @@ -133,10 +87,10 @@ namespace NzbDrone.Core.Test.MediaFiles { VerifyData(); - var files = Builder.CreateListOfSize(2) + var files = Builder.CreateListOfSize(2) .All() .With(c => c.Id = 0) - .With(c => c.Quality = new QualityModel(Quality.MP3_192)) + .With(c => c.Quality = new QualityModel(Quality.MP3_320)) .TheFirst(1) .With(c => c.Path = @"C:\Test\Path2\Artist\somefile1.flac".AsOsAgnostic()) .TheNext(1) @@ -155,25 +109,12 @@ namespace NzbDrone.Core.Test.MediaFiles var file = Subject.GetFileWithPath(@"C:\Test\Path\Artist\somefile2.flac".AsOsAgnostic()); file.Should().NotBeNull(); - file.Tracks.IsLoaded.Should().BeTrue(); - file.Tracks.Value.Should().NotBeNull(); - file.Tracks.Value.Should().NotBeEmpty(); file.Album.IsLoaded.Should().BeTrue(); file.Album.Value.Should().NotBeNull(); file.Artist.IsLoaded.Should().BeTrue(); file.Artist.Value.Should().NotBeNull(); } - [Test] - public void get_files_by_artist_should_only_return_tracks_for_monitored_releases() - { - VerifyData(); - var artistFiles = Subject.GetFilesByArtist(_artist.Id); - VerifyEagerLoaded(artistFiles); - - artistFiles.Should().HaveCount(4); - } - [Test] public void get_files_by_album() { @@ -181,34 +122,20 @@ namespace NzbDrone.Core.Test.MediaFiles var files = Subject.GetFilesByAlbum(_album.Id); VerifyEagerLoaded(files); - files.Should().OnlyContain(c => c.AlbumId == _album.Id); - } - - [Test] - public void get_files_by_album_should_only_return_tracks_for_monitored_releases() - { - VerifyData(); - var files = Subject.GetFilesByAlbum(_album.Id); - VerifyEagerLoaded(files); - - files.Should().HaveCount(4); + files.Should().OnlyContain(c => c.BookId == _album.Id); } private void VerifyData() { - Db.All().Should().HaveCount(1); - Db.All().Should().HaveCount(1); - Db.All().Should().HaveCount(10); - Db.All().Should().HaveCount(10); + Db.All().Should().HaveCount(1); + Db.All().Should().HaveCount(1); + Db.All().Should().HaveCount(10); } - private void VerifyEagerLoaded(List files) + private void VerifyEagerLoaded(List files) { foreach (var file in files) { - file.Tracks.IsLoaded.Should().BeTrue(); - file.Tracks.Value.Should().NotBeNull(); - file.Tracks.Value.Should().NotBeEmpty(); file.Album.IsLoaded.Should().BeTrue(); file.Album.Value.Should().NotBeNull(); file.Artist.IsLoaded.Should().BeTrue(); @@ -218,13 +145,10 @@ namespace NzbDrone.Core.Test.MediaFiles } } - private void VerifyUnmapped(List files) + private void VerifyUnmapped(List files) { foreach (var file in files) { - file.Tracks.IsLoaded.Should().BeFalse(); - file.Tracks.Value.Should().NotBeNull(); - file.Tracks.Value.Should().BeEmpty(); file.Album.IsLoaded.Should().BeFalse(); file.Album.Value.Should().BeNull(); file.Artist.IsLoaded.Should().BeFalse(); @@ -238,7 +162,7 @@ namespace NzbDrone.Core.Test.MediaFiles Db.Delete(_album); Subject.DeleteFilesByAlbum(_album.Id); - Db.All().Where(x => x.AlbumId == _album.Id).Should().HaveCount(0); + Db.All().Where(x => x.BookId == _album.Id).Should().HaveCount(0); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs index 90cd316df..324f20626 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs @@ -7,6 +7,7 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Music; using NzbDrone.Core.Test.Framework; @@ -42,7 +43,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(new List()); + .Returns(new List()); Subject.FilterUnchangedFiles(files, filter).Should().BeEquivalentTo(files); } @@ -60,7 +61,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(files.Select(f => new TrackFile + .Returns(files.Select(f => new BookFile { Path = f.FullName, Modified = _lastWrite @@ -82,9 +83,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(new List + .Returns(new List { - new TrackFile + new BookFile { Path = "C:\\file2.avi".AsOsAgnostic(), Modified = _lastWrite @@ -110,9 +111,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(new List + .Returns(new List { - new TrackFile + new BookFile { Path = "C:\\file2.avi".AsOsAgnostic(), Modified = _lastWrite @@ -138,9 +139,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(new List + .Returns(new List { - new TrackFile + new BookFile { Path = "C:\\file2.avi".AsOsAgnostic(), Modified = _lastWrite @@ -161,7 +162,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(new List()); + .Returns(new List()); Subject.FilterUnchangedFiles(files, filter).Should().HaveCount(1); Subject.FilterUnchangedFiles(files, filter).Select(x => x.FullName).Should().NotContain(files.First().FullName.ToLower()); @@ -180,9 +181,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(new List + .Returns(new List { - new TrackFile + new BookFile { Path = "C:\\file2.avi".AsOsAgnostic(), Size = 10, @@ -205,14 +206,14 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(new List + .Returns(new List { - new TrackFile + new BookFile { Path = "C:\\file2.avi".AsOsAgnostic(), Size = 10, Modified = _lastWrite, - Tracks = new List() + Album = new LazyLoaded(null) } }); @@ -231,14 +232,14 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(new List + .Returns(new List { - new TrackFile + new BookFile { Path = "C:\\file2.avi".AsOsAgnostic(), Size = 10, Modified = _lastWrite, - Tracks = Builder.CreateListOfSize(1).Build() as List + Album = Builder.CreateNew().Build() } }); @@ -258,9 +259,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests Mocker.GetMock() .Setup(c => c.GetFileWithPath(It.IsAny>())) - .Returns(new List + .Returns(new List { - new TrackFile + new BookFile { Path = "C:\\file2.avi".AsOsAgnostic(), Size = 10, diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/MediaFileServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/MediaFileServiceFixture.cs index fc6f7a5c3..0013dc8cc 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/MediaFileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/MediaFileServiceFixture.cs @@ -13,20 +13,20 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackFileMovingServiceTests [TestFixture] public class MediaFileServiceFixture : CoreTest { - private Album _album; - private List _trackFiles; + private Book _album; + private List _trackFiles; [SetUp] public void Setup() { - _album = Builder.CreateNew() + _album = Builder.CreateNew() .Build(); - _trackFiles = Builder.CreateListOfSize(3) + _trackFiles = Builder.CreateListOfSize(3) .TheFirst(2) - .With(f => f.AlbumId = _album.Id) + .With(f => f.BookId = _album.Id) .TheNext(1) - .With(f => f.AlbumId = 0) + .With(f => f.BookId = 0) .Build().ToList(); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs index 646291f74..cfd5e92f2 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs @@ -14,26 +14,22 @@ namespace NzbDrone.Core.Test.MediaFiles public class MediaFileTableCleanupServiceFixture : CoreTest { private readonly string _DELETED_PATH = @"c:\ANY FILE STARTING WITH THIS PATH IS CONSIDERED DELETED!".AsOsAgnostic(); - private List _tracks; - private Artist _artist; + private List _tracks; + private Author _artist; [SetUp] public void SetUp() { - _tracks = Builder.CreateListOfSize(10) + _tracks = Builder.CreateListOfSize(10) .Build() .ToList(); - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(s => s.Path = @"C:\Test\Music\Artist".AsOsAgnostic()) .Build(); - - Mocker.GetMock() - .Setup(c => c.GetTracksByFileId(It.IsAny>())) - .Returns((IEnumerable ids) => _tracks.Where(y => ids.Contains(y.TrackFileId)).ToList()); } - private void GivenTrackFiles(IEnumerable trackFiles) + private void GivenTrackFiles(IEnumerable trackFiles) { Mocker.GetMock() .Setup(c => c.GetFilesWithBasePath(It.IsAny())) @@ -42,12 +38,9 @@ namespace NzbDrone.Core.Test.MediaFiles private void GivenFilesAreNotAttachedToTrack() { - Mocker.GetMock() - .Setup(c => c.GetTracksByFileId(It.IsAny())) - .Returns(new List()); } - private List FilesOnDisk(IEnumerable trackFiles) + private List FilesOnDisk(IEnumerable trackFiles) { return trackFiles.Select(e => e.Path).ToList(); } @@ -55,7 +48,7 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_skip_files_that_exist_on_disk() { - var trackFiles = Builder.CreateListOfSize(10) + var trackFiles = Builder.CreateListOfSize(10) .All() .With(x => x.Path = Path.Combine(@"c:\test".AsOsAgnostic(), Path.GetRandomFileName())) .Build(); @@ -65,13 +58,13 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Clean(_artist.Path, FilesOnDisk(trackFiles)); Mocker.GetMock() - .Verify(c => c.DeleteMany(It.Is>(x => x.Count == 0), DeleteMediaFileReason.MissingFromDisk), Times.Once()); + .Verify(c => c.DeleteMany(It.Is>(x => x.Count == 0), DeleteMediaFileReason.MissingFromDisk), Times.Once()); } [Test] public void should_delete_non_existent_files() { - var trackFiles = Builder.CreateListOfSize(10) + var trackFiles = Builder.CreateListOfSize(10) .All() .With(x => x.Path = Path.Combine(@"c:\test".AsOsAgnostic(), Path.GetRandomFileName())) .Random(2) @@ -83,13 +76,13 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Clean(_artist.Path, FilesOnDisk(trackFiles.Where(e => !e.Path.StartsWith(_DELETED_PATH)))); Mocker.GetMock() - .Verify(c => c.DeleteMany(It.Is>(e => e.Count == 2 && e.All(y => y.Path.StartsWith(_DELETED_PATH))), DeleteMediaFileReason.MissingFromDisk), Times.Once()); + .Verify(c => c.DeleteMany(It.Is>(e => e.Count == 2 && e.All(y => y.Path.StartsWith(_DELETED_PATH))), DeleteMediaFileReason.MissingFromDisk), Times.Once()); } [Test] public void should_unlink_track_when_trackFile_does_not_exist() { - var trackFiles = Builder.CreateListOfSize(10) + var trackFiles = Builder.CreateListOfSize(10) .Random(10) .With(c => c.Path = Path.Combine(@"c:\test".AsOsAgnostic(), Path.GetRandomFileName())) .Build(); @@ -97,15 +90,12 @@ namespace NzbDrone.Core.Test.MediaFiles GivenTrackFiles(trackFiles); Subject.Clean(_artist.Path, new List()); - - Mocker.GetMock() - .Verify(c => c.SetFileIds(It.Is>(e => e.Count == 10 && e.All(y => y.TrackFileId == 0))), Times.Once()); } [Test] public void should_not_update_track_when_trackFile_exists() { - var trackFiles = Builder.CreateListOfSize(10) + var trackFiles = Builder.CreateListOfSize(10) .Random(10) .With(c => c.Path = Path.Combine(@"c:\test".AsOsAgnostic(), Path.GetRandomFileName())) .Build(); @@ -113,8 +103,6 @@ namespace NzbDrone.Core.Test.MediaFiles GivenTrackFiles(trackFiles); Subject.Clean(_artist.Path, FilesOnDisk(trackFiles)); - - Mocker.GetMock().Verify(c => c.SetFileIds(It.Is>(x => x.Count == 0)), Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/RenameTrackFileServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/RenameTrackFileServiceFixture.cs index 79d79d376..988d56ed8 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/RenameTrackFileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/RenameTrackFileServiceFixture.cs @@ -14,16 +14,16 @@ namespace NzbDrone.Core.Test.MediaFiles { public class RenameTrackFileServiceFixture : CoreTest { - private Artist _artist; - private List _trackFiles; + private Author _artist; + private List _trackFiles; [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .Build(); - _trackFiles = Builder.CreateListOfSize(2) + _trackFiles = Builder.CreateListOfSize(2) .All() .With(e => e.Artist = _artist) .Build() @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.MediaFiles { Mocker.GetMock() .Setup(s => s.Get(It.IsAny>())) - .Returns(new List()); + .Returns(new List()); } private void GivenTrackFiles() @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.MediaFiles private void GivenMovedFiles() { Mocker.GetMock() - .Setup(s => s.MoveTrackFile(It.IsAny(), _artist)); + .Setup(s => s.MoveTrackFile(It.IsAny(), _artist)); } [Test] @@ -71,7 +71,7 @@ namespace NzbDrone.Core.Test.MediaFiles GivenTrackFiles(); Mocker.GetMock() - .Setup(s => s.MoveTrackFile(It.IsAny(), It.IsAny())) + .Setup(s => s.MoveTrackFile(It.IsAny(), It.IsAny())) .Throws(new SameFilenameException("Same file name", "Filename")); Subject.Execute(new RenameFilesCommand(_artist.Id, new List { 1 })); @@ -101,7 +101,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Execute(new RenameFilesCommand(_artist.Id, new List { 1 })); Mocker.GetMock() - .Verify(v => v.Update(It.IsAny()), Times.Exactly(2)); + .Verify(v => v.Update(It.IsAny()), Times.Exactly(2)); } [Test] diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackFileMovingServiceTests/MoveTrackFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackFileMovingServiceTests/MoveTrackFileFixture.cs index 4136c498f..4d26d95e5 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackFileMovingServiceTests/MoveTrackFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackFileMovingServiceTests/MoveTrackFileFixture.cs @@ -21,37 +21,37 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackFileMovingServiceTests [TestFixture] public class MoveTrackFileFixture : CoreTest { - private Artist _artist; - private TrackFile _trackFile; + private Author _artist; + private BookFile _trackFile; private LocalTrack _localtrack; [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(s => s.Path = @"C:\Test\Music\Artist".AsOsAgnostic()) .Build(); - _trackFile = Builder.CreateNew() + _trackFile = Builder.CreateNew() .With(f => f.Path = null) .With(f => f.Path = Path.Combine(_artist.Path, @"Album\File.mp3")) .Build(); _localtrack = Builder.CreateNew() .With(l => l.Artist = _artist) - .With(l => l.Tracks = Builder.CreateListOfSize(1).Build().ToList()) + .With(l => l.Album = Builder.CreateNew().Build()) .Build(); Mocker.GetMock() - .Setup(s => s.BuildTrackFileName(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), null, null)) + .Setup(s => s.BuildTrackFileName(It.IsAny(), It.IsAny(), It.IsAny(), null, null)) .Returns("File Name"); Mocker.GetMock() - .Setup(s => s.BuildTrackFilePath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.BuildTrackFilePath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(@"C:\Test\Music\Artist\Album\File Name.mp3".AsOsAgnostic()); Mocker.GetMock() - .Setup(s => s.BuildAlbumPath(It.IsAny(), It.IsAny())) + .Setup(s => s.BuildAlbumPath(It.IsAny(), It.IsAny())) .Returns(@"C:\Test\Music\Artist\Album".AsOsAgnostic()); var rootFolder = @"C:\Test\Music\".AsOsAgnostic(); diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/AlbumDistanceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/AlbumDistanceFixture.cs deleted file mode 100644 index 687b84923..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/AlbumDistanceFixture.cs +++ /dev/null @@ -1,263 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.MediaFiles.TrackImport.Identification; -using NzbDrone.Core.Music; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification -{ - [TestFixture] - public class AlbumDistanceFixture : CoreTest - { - private ArtistMetadata _artist; - - [SetUp] - public void Setup() - { - _artist = Builder - .CreateNew() - .With(x => x.Name = "artist") - .Build(); - } - - private List GivenTracks(int count) - { - return Builder - .CreateListOfSize(count) - .All() - .With(x => x.ArtistMetadata = _artist) - .With(x => x.MediumNumber = 1) - .Build() - .ToList(); - } - - private LocalTrack GivenLocalTrack(Track track, AlbumRelease release) - { - var fileInfo = Builder - .CreateNew() - .With(x => x.Title = track.Title) - .With(x => x.CleanTitle = track.Title.CleanTrackTitle()) - .With(x => x.AlbumTitle = release.Title) - .With(x => x.Disambiguation = release.Disambiguation) - .With(x => x.ReleaseMBId = release.ForeignReleaseId) - .With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name) - .With(x => x.TrackNumbers = new[] { track.AbsoluteTrackNumber }) - .With(x => x.DiscCount = release.Media.Count) - .With(x => x.DiscNumber = track.MediumNumber) - .With(x => x.RecordingMBId = track.ForeignRecordingId) - .With(x => x.Country = IsoCountries.Find("US")) - .With(x => x.Label = release.Label.First()) - .With(x => x.Year = (uint)(release.Album.Value.ReleaseDate?.Year ?? 0)) - .Build(); - - var localTrack = Builder - .CreateNew() - .With(x => x.FileTrackInfo = fileInfo) - .Build(); - - return localTrack; - } - - private List GivenLocalTracks(List tracks, AlbumRelease release) - { - var output = new List(); - foreach (var track in tracks) - { - output.Add(GivenLocalTrack(track, release)); - } - - return output; - } - - private AlbumRelease GivenAlbumRelease(string title, List tracks) - { - var album = Builder - .CreateNew() - .With(x => x.Title = title) - .With(x => x.ArtistMetadata = _artist) - .Build(); - - var media = Builder - .CreateListOfSize(tracks.Max(x => x.MediumNumber)) - .Build() - .ToList(); - - return Builder - .CreateNew() - .With(x => x.Tracks = tracks) - .With(x => x.Title = title) - .With(x => x.Album = album) - .With(x => x.Media = media) - .With(x => x.Country = new List { "United States" }) - .With(x => x.Label = new List { "label" }) - .Build(); - } - - private TrackMapping GivenMapping(List local, List remote) - { - var mapping = new TrackMapping(); - var distances = local.Zip(remote, (l, r) => Tuple.Create(r, DistanceCalculator.TrackDistance(l, r, DistanceCalculator.GetTotalTrackNumber(r, remote)))); - mapping.Mapping = local.Zip(distances, (l, r) => new { l, r }).ToDictionary(x => x.l, x => x.r); - mapping.LocalExtra = local.Except(mapping.Mapping.Keys).ToList(); - mapping.MBExtra = remote.Except(mapping.Mapping.Values.Select(x => x.Item1)).ToList(); - - return mapping; - } - - [Test] - public void test_identical_albums() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - var mapping = GivenMapping(localTracks, tracks); - - DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0); - } - - [Test] - public void test_incomplete_album() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - localTracks.RemoveAt(1); - var mapping = GivenMapping(localTracks, tracks); - - var dist = DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping); - dist.NormalizedDistance().Should().NotBe(0.0); - dist.NormalizedDistance().Should().BeLessThan(0.2); - } - - [Test] - public void test_global_artists_differ() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - var mapping = GivenMapping(localTracks, tracks); - - release.Album.Value.ArtistMetadata = Builder - .CreateNew() - .With(x => x.Name = "different artist") - .Build(); - - DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().NotBe(0.0); - } - - [Test] - public void test_comp_track_artists_match() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - var mapping = GivenMapping(localTracks, tracks); - - release.Album.Value.ArtistMetadata = Builder - .CreateNew() - .With(x => x.Name = "Various Artists") - .With(x => x.ForeignArtistId = "89ad4ac3-39f7-470e-963a-56509c546377") - .Build(); - - DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0); - } - - // TODO: there are a couple more VA tests in beets but we don't support VA yet anyway - [Test] - public void test_tracks_out_of_order() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - localTracks = new[] { 1, 3, 2 }.Select(x => localTracks[x - 1]).ToList(); - var mapping = GivenMapping(localTracks, tracks); - - var dist = DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping); - dist.NormalizedDistance().Should().NotBe(0.0); - dist.NormalizedDistance().Should().BeLessThan(0.2); - } - - [Test] - public void test_two_medium_release() - { - var tracks = GivenTracks(3); - tracks[2].AbsoluteTrackNumber = 1; - tracks[2].MediumNumber = 2; - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - var mapping = GivenMapping(localTracks, tracks); - - DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0); - } - - [Test] - public void test_absolute_track_numbering() - { - var tracks = GivenTracks(3); - tracks[2].AbsoluteTrackNumber = 1; - tracks[2].MediumNumber = 2; - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - localTracks[2].FileTrackInfo.DiscNumber = 2; - localTracks[2].FileTrackInfo.TrackNumbers = new[] { 3 }; - - var mapping = GivenMapping(localTracks, tracks); - - DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0); - } - - private static DateTime?[] dates = new DateTime?[] { null, new DateTime(2007, 1, 1), DateTime.Now }; - - [TestCaseSource("dates")] - public void test_null_album_year(DateTime? releaseDate) - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - var mapping = GivenMapping(localTracks, tracks); - - release.Album.Value.ReleaseDate = null; - release.ReleaseDate = releaseDate; - - var result = DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance(); - - if (!releaseDate.HasValue || (localTracks[0].FileTrackInfo.Year == (releaseDate?.Year ?? 0))) - { - result.Should().Be(0.0); - } - else - { - result.Should().NotBe(0.0); - } - } - - [TestCaseSource("dates")] - public void test_null_release_year(DateTime? albumDate) - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - var mapping = GivenMapping(localTracks, tracks); - - release.Album.Value.ReleaseDate = albumDate; - release.ReleaseDate = null; - - var result = DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance(); - - if (!albumDate.HasValue || (localTracks[0].FileTrackInfo.Year == (albumDate?.Year ?? 0))) - { - result.Should().Be(0.0); - } - else - { - result.Should().NotBe(0.0); - } - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/CandidateServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/CandidateServiceFixture.cs deleted file mode 100644 index b016be75c..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/CandidateServiceFixture.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.MediaFiles.TrackImport; -using NzbDrone.Core.MediaFiles.TrackImport.Identification; -using NzbDrone.Core.Music; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification -{ - [TestFixture] - public class GetCandidatesFixture : CoreTest - { - private ArtistMetadata _artist; - - [SetUp] - public void Setup() - { - _artist = Builder - .CreateNew() - .With(x => x.Name = "artist") - .Build(); - } - - private List GivenTracks(int count) - { - return Builder - .CreateListOfSize(count) - .All() - .With(x => x.ArtistMetadata = _artist) - .Build() - .ToList(); - } - - private ParsedTrackInfo GivenParsedTrackInfo(Track track, AlbumRelease release) - { - return Builder - .CreateNew() - .With(x => x.Title = track.Title) - .With(x => x.AlbumTitle = release.Title) - .With(x => x.Disambiguation = release.Disambiguation) - .With(x => x.ReleaseMBId = release.ForeignReleaseId) - .With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name) - .With(x => x.TrackNumbers = new[] { track.AbsoluteTrackNumber }) - .With(x => x.RecordingMBId = track.ForeignRecordingId) - .With(x => x.Country = IsoCountries.Find("US")) - .With(x => x.Label = release.Label.First()) - .With(x => x.Year = (uint)release.Album.Value.ReleaseDate.Value.Year) - .Build(); - } - - private List GivenLocalTracks(List tracks, AlbumRelease release) - { - var output = Builder - .CreateListOfSize(tracks.Count) - .Build() - .ToList(); - - for (int i = 0; i < tracks.Count; i++) - { - output[i].FileTrackInfo = GivenParsedTrackInfo(tracks[i], release); - } - - return output; - } - - private AlbumRelease GivenAlbumRelease(string title, List tracks) - { - var album = Builder - .CreateNew() - .With(x => x.Title = title) - .With(x => x.ArtistMetadata = _artist) - .Build(); - - var media = Builder - .CreateListOfSize(1) - .Build() - .ToList(); - - return Builder - .CreateNew() - .With(x => x.Tracks = tracks) - .With(x => x.Title = title) - .With(x => x.Album = album) - .With(x => x.Media = media) - .With(x => x.Country = new List()) - .With(x => x.Label = new List { "label" }) - .With(x => x.ForeignReleaseId = null) - .Build(); - } - - private LocalAlbumRelease GivenLocalAlbumRelease() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - - return new LocalAlbumRelease(localTracks); - } - - [Test] - public void get_candidates_by_fingerprint_should_not_fail_if_fingerprint_lookup_returned_null() - { - Mocker.GetMock() - .Setup(x => x.Lookup(It.IsAny>(), It.IsAny())) - .Callback((List x, double thres) => - { - foreach (var track in x) - { - track.AcoustIdResults = null; - } - }); - - Mocker.GetMock() - .Setup(x => x.GetReleasesByRecordingIds(It.IsAny>())) - .Returns(new List()); - - var local = GivenLocalAlbumRelease(); - - Subject.GetDbCandidatesFromFingerprint(local, null, false).Should().BeEquivalentTo(new List()); - } - - [Test] - public void get_candidates_should_only_return_specified_release_if_set() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - var localAlbumRelease = new LocalAlbumRelease(localTracks); - var idOverrides = new IdentificationOverrides - { - AlbumRelease = release - }; - - Subject.GetDbCandidatesFromTags(localAlbumRelease, idOverrides, false).Should().BeEquivalentTo( - new List { new CandidateAlbumRelease(release) }); - } - - [Test] - public void get_candidates_should_use_consensus_release_id() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - release.ForeignReleaseId = "xxx"; - var localTracks = GivenLocalTracks(tracks, release); - var localAlbumRelease = new LocalAlbumRelease(localTracks); - - Mocker.GetMock() - .Setup(x => x.GetReleaseByForeignReleaseId("xxx", true)) - .Returns(release); - - Subject.GetDbCandidatesFromTags(localAlbumRelease, null, false).Should().BeEquivalentTo( - new List { new CandidateAlbumRelease(release) }); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/IdentificationServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/IdentificationServiceFixture.cs index 24481908d..d1ff79569 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/IdentificationServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/IdentificationServiceFixture.cs @@ -46,8 +46,6 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); - Mocker.SetConstant(Mocker.Resolve()); - Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); @@ -57,23 +55,19 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification Mocker.SetConstant(_artistService); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); - Mocker.SetConstant(Mocker.Resolve()); - Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); - Mocker.SetConstant(Mocker.Resolve()); - Mocker.SetConstant(Mocker.Resolve()); + Mocker.SetConstant(Mocker.Resolve()); + Mocker.SetConstant(Mocker.Resolve()); _addArtistService = Mocker.Resolve(); - Mocker.SetConstant(Mocker.Resolve()); - Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); _refreshArtistService = Mocker.Resolve(); - Mocker.GetMock().Setup(x => x.Validate(It.IsAny())).Returns(new ValidationResult()); + Mocker.GetMock().Setup(x => x.Validate(It.IsAny())).Returns(new ValidationResult()); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); @@ -94,9 +88,9 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification Mocker.GetMock().Setup(x => x.Get(profile.Id)).Returns(profile); } - private List GivenArtists(List artists) + private List GivenArtists(List artists) { - var outp = new List(); + var outp = new List(); for (int i = 0; i < artists.Count; i++) { var meta = artists[i].MetadataProfile; @@ -108,13 +102,13 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification return outp; } - private Artist GivenArtist(string foreignArtistId, int metadataProfileId) + private Author GivenArtist(string foreignAuthorId, int metadataProfileId) { - var artist = _addArtistService.AddArtist(new Artist + var artist = _addArtistService.AddArtist(new Author { - Metadata = new ArtistMetadata + Metadata = new AuthorMetadata { - ForeignArtistId = foreignArtistId + ForeignAuthorId = foreignAuthorId }, Path = @"c:\test".AsOsAgnostic(), MetadataProfileId = metadataProfileId @@ -122,13 +116,13 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification var command = new RefreshArtistCommand { - ArtistId = artist.Id, + AuthorId = artist.Id, Trigger = CommandTrigger.Unspecified }; _refreshArtistService.Execute(command); - return _artistService.FindById(foreignArtistId); + return _artistService.FindById(foreignAuthorId); } private void GivenFingerprints(List fingerprints) @@ -179,7 +173,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification var testcase = JsonConvert.DeserializeObject(File.ReadAllText(path)); var artists = GivenArtists(testcase.LibraryArtists); - var specifiedArtist = artists.SingleOrDefault(x => x.Metadata.Value.ForeignArtistId == testcase.Artist); + var specifiedArtist = artists.SingleOrDefault(x => x.Metadata.Value.ForeignAuthorId == testcase.Artist); var idOverrides = new IdentificationOverrides { Artist = specifiedArtist }; var tracks = testcase.Tracks.Select(x => new LocalTrack @@ -202,10 +196,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification var result = _Subject.Identify(tracks, idOverrides, config); - TestLogger.Debug($"Found releases:\n{result.Where(x => x.AlbumRelease != null).Select(x => x.AlbumRelease?.ForeignReleaseId).ToJson()}"); - result.Should().HaveCount(testcase.ExpectedMusicBrainzReleaseIds.Count); - result.Where(x => x.AlbumRelease != null).Select(x => x.AlbumRelease.ForeignReleaseId).Should().BeEquivalentTo(testcase.ExpectedMusicBrainzReleaseIds); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/TrackDistanceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/TrackDistanceFixture.cs deleted file mode 100644 index fa882f932..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/TrackDistanceFixture.cs +++ /dev/null @@ -1,99 +0,0 @@ -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.MediaFiles.TrackImport.Identification; -using NzbDrone.Core.Music; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification -{ - [TestFixture] - public class TrackDistanceFixture : CoreTest - { - private Track GivenTrack(string title) - { - var artist = Builder - .CreateNew() - .With(x => x.Name = "artist") - .Build(); - - var mbTrack = Builder - .CreateNew() - .With(x => x.Title = title) - .With(x => x.ArtistMetadata = artist) - .Build(); - - return mbTrack; - } - - private LocalTrack GivenLocalTrack(Track track) - { - var fileInfo = Builder - .CreateNew() - .With(x => x.Title = track.Title) - .With(x => x.CleanTitle = track.Title.CleanTrackTitle()) - .With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name) - .With(x => x.TrackNumbers = new[] { 1 }) - .With(x => x.RecordingMBId = track.ForeignRecordingId) - .Build(); - - var localTrack = Builder - .CreateNew() - .With(x => x.FileTrackInfo = fileInfo) - .Build(); - - return localTrack; - } - - [Test] - public void test_identical_tracks() - { - var track = GivenTrack("one"); - var localTrack = GivenLocalTrack(track); - - DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0); - } - - [Test] - public void test_feat_removed_from_localtrack() - { - var track = GivenTrack("one"); - var localTrack = GivenLocalTrack(track); - localTrack.FileTrackInfo.Title = "one (feat. two)"; - - DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0); - } - - [Test] - public void test_different_title() - { - var track = GivenTrack("one"); - var localTrack = GivenLocalTrack(track); - localTrack.FileTrackInfo.CleanTitle = "foo"; - - DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0); - } - - [Test] - public void test_different_artist() - { - var track = GivenTrack("one"); - var localTrack = GivenLocalTrack(track); - localTrack.FileTrackInfo.ArtistTitle = "foo"; - - DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0); - } - - [Test] - public void test_various_artists_tolerated() - { - var track = GivenTrack("one"); - var localTrack = GivenLocalTrack(track); - localTrack.FileTrackInfo.ArtistTitle = "Various Artists"; - - DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/TrackMappingFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/TrackMappingFixture.cs deleted file mode 100644 index c1445bdb9..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/TrackMappingFixture.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.MediaFiles.TrackImport.Identification; -using NzbDrone.Core.Music; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification -{ - [TestFixture] - public class TrackMappingFixture : CoreTest - { - private ArtistMetadata _artist; - - [SetUp] - public void Setup() - { - _artist = Builder - .CreateNew() - .With(x => x.Name = "artist") - .Build(); - } - - private List GivenTracks(int count) - { - return Builder - .CreateListOfSize(count) - .All() - .With(x => x.ArtistMetadata = _artist) - .Build() - .ToList(); - } - - private ParsedTrackInfo GivenParsedTrackInfo(Track track, AlbumRelease release) - { - return Builder - .CreateNew() - .With(x => x.Title = track.Title) - .With(x => x.CleanTitle = track.Title.CleanTrackTitle()) - .With(x => x.AlbumTitle = release.Title) - .With(x => x.Disambiguation = release.Disambiguation) - .With(x => x.ReleaseMBId = release.ForeignReleaseId) - .With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name) - .With(x => x.TrackNumbers = new[] { track.AbsoluteTrackNumber }) - .With(x => x.RecordingMBId = track.ForeignRecordingId) - .With(x => x.Country = IsoCountries.Find("US")) - .With(x => x.Label = release.Label.First()) - .With(x => x.Year = (uint)release.Album.Value.ReleaseDate.Value.Year) - .Build(); - } - - private List GivenLocalTracks(List tracks, AlbumRelease release) - { - var output = Builder - .CreateListOfSize(tracks.Count) - .Build() - .ToList(); - - for (int i = 0; i < tracks.Count; i++) - { - output[i].FileTrackInfo = GivenParsedTrackInfo(tracks[i], release); - } - - return output; - } - - private AlbumRelease GivenAlbumRelease(string title, List tracks) - { - var album = Builder - .CreateNew() - .With(x => x.Title = title) - .With(x => x.ArtistMetadata = _artist) - .Build(); - - var media = Builder - .CreateListOfSize(1) - .Build() - .ToList(); - - return Builder - .CreateNew() - .With(x => x.Tracks = tracks) - .With(x => x.Title = title) - .With(x => x.Album = album) - .With(x => x.Media = media) - .With(x => x.Country = new List()) - .With(x => x.Label = new List { "label" }) - .Build(); - } - - [Test] - public void test_reorder_when_track_numbers_incorrect() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - - localTracks[2].FileTrackInfo.TrackNumbers = new[] { 2 }; - localTracks[1].FileTrackInfo.TrackNumbers = new[] { 3 }; - localTracks = new[] { 0, 2, 1 }.Select(x => localTracks[x]).ToList(); - - var result = Subject.MapReleaseTracks(localTracks, tracks); - - result.Mapping - .ToDictionary(x => x.Key, y => y.Value.Item1) - .Should().BeEquivalentTo(new Dictionary - { - { localTracks[0], tracks[0] }, - { localTracks[1], tracks[2] }, - { localTracks[2], tracks[1] }, - }); - result.LocalExtra.Should().BeEmpty(); - result.MBExtra.Should().BeEmpty(); - } - - [Test] - public void test_order_works_with_invalid_track_numbers() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - - foreach (var track in localTracks) - { - track.FileTrackInfo.TrackNumbers = new[] { 1 }; - } - - var result = Subject.MapReleaseTracks(localTracks, tracks); - - result.Mapping - .ToDictionary(x => x.Key, y => y.Value.Item1) - .Should().BeEquivalentTo(new Dictionary - { - { localTracks[0], tracks[0] }, - { localTracks[1], tracks[1] }, - { localTracks[2], tracks[2] }, - }); - result.LocalExtra.Should().BeEmpty(); - result.MBExtra.Should().BeEmpty(); - } - - [Test] - public void test_order_works_with_missing_tracks() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - localTracks.RemoveAt(1); - - var result = Subject.MapReleaseTracks(localTracks, tracks); - - result.Mapping - .ToDictionary(x => x.Key, y => y.Value.Item1) - .Should().BeEquivalentTo(new Dictionary - { - { localTracks[0], tracks[0] }, - { localTracks[1], tracks[2] } - }); - result.LocalExtra.Should().BeEmpty(); - result.MBExtra.Should().BeEquivalentTo(new List { tracks[1] }); - } - - [Test] - public void test_order_works_with_extra_tracks() - { - var tracks = GivenTracks(3); - var release = GivenAlbumRelease("album", tracks); - var localTracks = GivenLocalTracks(tracks, release); - tracks.RemoveAt(1); - - var result = Subject.MapReleaseTracks(localTracks, tracks); - - result.Mapping - .ToDictionary(x => x.Key, y => y.Value.Item1) - .Should().BeEquivalentTo(new Dictionary - { - { localTracks[0], tracks[0] }, - { localTracks[2], tracks[1] } - }); - result.LocalExtra.Should().BeEquivalentTo(new List { localTracks[1] }); - result.MBExtra.Should().BeEmpty(); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs index 0b3698c79..9c27350cc 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs @@ -26,9 +26,8 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport { private List _fileInfos; private LocalTrack _localTrack; - private Artist _artist; - private Album _album; - private AlbumRelease _albumRelease; + private Author _artist; + private Book _album; private QualityModel _quality; private IdentificationOverrides _idOverrides; @@ -85,26 +84,22 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny(), It.IsAny())).Returns(Decision.Reject("_fail2")); _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny(), It.IsAny())).Returns(Decision.Reject("_fail3")); - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(e => e.QualityProfileId = 1) .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); - _album = Builder.CreateNew() - .With(x => x.Artist = _artist) + _album = Builder.CreateNew() + .With(x => x.Author = _artist) .Build(); - _albumRelease = Builder.CreateNew() - .With(x => x.Album = _album) - .Build(); - - _quality = new QualityModel(Quality.MP3_256); + _quality = new QualityModel(Quality.MP3_320); _localTrack = new LocalTrack { Artist = _artist, Quality = _quality, - Tracks = new List { new Track() }, + Album = new Book(), Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() }; @@ -122,7 +117,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport .Returns((List tracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config) => { var ret = new LocalAlbumRelease(tracks); - ret.AlbumRelease = _albumRelease; + ret.Book = _album; return new List { ret }; }); @@ -154,7 +149,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport .Setup(s => s.Augment(It.IsAny(), It.IsAny())) .Callback((localTrack, otherFiles) => { - localTrack.Tracks = _localTrack.Tracks; + localTrack.Album = _localTrack.Album; }); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/FreeSpaceSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/FreeSpaceSpecificationFixture.cs index 73073f617..823bf3448 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/FreeSpaceSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/FreeSpaceSpecificationFixture.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using Moq; @@ -17,7 +16,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications [TestFixture] public class FreeSpaceSpecificationFixture : CoreTest { - private Artist _artist; + private Author _artist; private LocalTrack _localTrack; private string _rootFolder; @@ -26,19 +25,14 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications { _rootFolder = @"C:\Test\Music".AsOsAgnostic(); - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(s => s.Path = Path.Combine(_rootFolder, "Alice in Chains")) .Build(); - var tracks = Builder.CreateListOfSize(1) - .All() - .Build() - .ToList(); - _localTrack = new LocalTrack { Path = @"C:\Test\Unsorted\Alice in Chains\Alice in Chains - track1.mp3".AsOsAgnostic(), - Tracks = tracks, + Album = new Book(), Artist = _artist }; } diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/NotUnpackingSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/NotUnpackingSpecificationFixture.cs index 3f8cbc32f..bcdde075b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/NotUnpackingSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/NotUnpackingSpecificationFixture.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications { Path = @"C:\Test\Unsorted Music\Kid.Rock\Kid.Rock.Cowboy.mp3".AsOsAgnostic(), Size = 100, - Artist = Builder.CreateNew().Build() + Artist = Builder.CreateNew().Build() }; } diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs index 920fec899..9d3b3dfcf 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; @@ -27,14 +27,13 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications [Test] public void should_be_accepted_if_no_existing_file() { - _localTrack.Tracks = Builder.CreateListOfSize(1) - .TheFirst(1) - .With(e => e.TrackFileId = 0) - .BuildList(); + _localTrack.Album = Builder.CreateNew() + .Build(); Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeTrue(); } + /* [Test] public void should_be_accepted_if_multiple_existing_files() { @@ -57,21 +56,21 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications .ToList(); Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeTrue(); - } + }*/ [Test] public void should_be_accepted_if_file_size_is_different() { - _localTrack.Tracks = Builder.CreateListOfSize(1) - .TheFirst(1) - .With(e => e.TrackFileId = 1) - .With(e => e.TrackFile = new LazyLoaded( - new TrackFile - { - Size = _localTrack.Size + 100.Megabytes() - })) - .Build() - .ToList(); + _localTrack.Album = Builder.CreateNew() + .With(e => e.BookFiles = new LazyLoaded>( + new List + { + new BookFile + { + Size = _localTrack.Size + 100.Megabytes() + } + })) + .Build(); Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeTrue(); } @@ -79,16 +78,16 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications [Test] public void should_be_reject_if_file_size_is_the_same() { - _localTrack.Tracks = Builder.CreateListOfSize(1) - .TheFirst(1) - .With(e => e.TrackFileId = 1) - .With(e => e.TrackFile = new LazyLoaded( - new TrackFile - { - Size = _localTrack.Size - })) - .Build() - .ToList(); + _localTrack.Album = Builder.CreateNew() + .With(e => e.BookFiles = new LazyLoaded>( + new List + { + new BookFile + { + Size = _localTrack.Size + } + })) + .Build(); Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs index 557596f74..315f029af 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs @@ -17,25 +17,26 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications [TestFixture] public class UpgradeSpecificationFixture : CoreTest { - private Artist _artist; - private Album _album; + /* + private Author _artist; + private Book _album; private LocalTrack _localTrack; [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities(), }).Build(); - _album = Builder.CreateNew().Build(); + _album = Builder.CreateNew().Build(); _localTrack = new LocalTrack { Path = @"C:\Test\Imagine Dragons\Imagine.Dragons.Song.1.mp3", - Quality = new QualityModel(Quality.MP3_256, new Revision(version: 1)), + Quality = new QualityModel(Quality.MP3_320, new Revision(version: 1)), Artist = _artist, Album = _album }; @@ -76,7 +77,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications .With(e => e.TrackFile = new LazyLoaded( new TrackFile { - Quality = new QualityModel(Quality.MP3_192, new Revision(version: 1)) + Quality = new QualityModel(Quality.MP3_320, new Revision(version: 1)) })) .Build() .ToList(); @@ -93,7 +94,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications .With(e => e.TrackFile = new LazyLoaded( new TrackFile { - Quality = new QualityModel(Quality.MP3_192, new Revision(version: 1)) + Quality = new QualityModel(Quality.MP3_320, new Revision(version: 1)) })) .Build() .ToList(); @@ -144,7 +145,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications .With(e => e.TrackFile = new LazyLoaded( new TrackFile { - Quality = new QualityModel(Quality.MP3_192, new Revision(version: 1)) + Quality = new QualityModel(Quality.MP3_320, new Revision(version: 1)) })) .TheNext(1) .With(e => e.TrackFileId = 2) @@ -172,7 +173,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications .With(e => e.TrackFile = new LazyLoaded( new TrackFile { - Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) + Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)) })) .Build() .ToList(); @@ -193,7 +194,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications .With(e => e.TrackFile = new LazyLoaded( new TrackFile { - Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) + Quality = new QualityModel(Quality.MP3_320, new Revision(version: 2)) })) .Build() .ToList(); @@ -236,5 +237,6 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeTrue(); } + */ } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs index da23bf1f9..397d3d028 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs @@ -1,5 +1,5 @@ +using System.Collections.Generic; using System.IO; -using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using Moq; @@ -9,6 +9,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Music; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; @@ -16,7 +17,7 @@ namespace NzbDrone.Core.Test.MediaFiles { public class UpgradeMediaFileServiceFixture : CoreTest { - private TrackFile _trackFile; + private BookFile _trackFile; private LocalTrack _localTrack; private string _rootPath = @"C:\Test\Music\Artist".AsOsAgnostic(); @@ -24,12 +25,12 @@ namespace NzbDrone.Core.Test.MediaFiles public void Setup() { _localTrack = new LocalTrack(); - _localTrack.Artist = new Artist + _localTrack.Artist = new Author { Path = _rootPath }; - _trackFile = Builder + _trackFile = Builder .CreateNew() .Build(); @@ -44,57 +45,25 @@ namespace NzbDrone.Core.Test.MediaFiles Mocker.GetMock() .Setup(c => c.GetParentFolder(It.IsAny())) .Returns(c => Path.GetDirectoryName(c)); - } - - private void GivenSingleTrackWithSingleTrackFile() - { - _localTrack.Tracks = Builder.CreateListOfSize(1) - .All() - .With(e => e.TrackFileId = 1) - .With(e => e.TrackFile = new LazyLoaded( - new TrackFile - { - Id = 1, - Path = Path.Combine(_rootPath, @"Season 01\30.rock.s01e01.avi"), - })) - .Build() - .ToList(); - } - private void GivenMultipleTracksWithSingleTrackFile() - { - _localTrack.Tracks = Builder.CreateListOfSize(2) - .All() - .With(e => e.TrackFileId = 1) - .With(e => e.TrackFile = new LazyLoaded( - new TrackFile - { - Id = 1, - Path = Path.Combine(_rootPath, @"Season 01\30.rock.s01e01.avi"), - })) - .Build() - .ToList(); + Mocker.GetMock() + .Setup(c => c.GetBestRootFolder(It.IsAny())) + .Returns(new RootFolder()); } - private void GivenMultipleTracksWithMultipleTrackFiles() + private void GivenSingleTrackWithSingleTrackFile() { - _localTrack.Tracks = Builder.CreateListOfSize(2) - .TheFirst(1) - .With(e => e.TrackFile = new LazyLoaded( - new TrackFile - { - Id = 1, - Path = Path.Combine(_rootPath, @"Season 01\30.rock.s01e01.avi"), - })) - .TheNext(1) - .With(e => e.TrackFile = new LazyLoaded( - new TrackFile - { - Id = 2, - Path = Path.Combine(_rootPath, @"Season 01\30.rock.s01e02.avi"), - })) - .Build() - .ToList(); + _localTrack.Album = Builder.CreateNew() + .With(e => e.BookFiles = new LazyLoaded>( + new List + { + new BookFile + { + Id = 1, + Path = Path.Combine(_rootPath, @"Season 01\30.rock.s01e01.avi"), + } + })) + .Build(); } [Test] @@ -107,26 +76,6 @@ namespace NzbDrone.Core.Test.MediaFiles Mocker.GetMock().Verify(v => v.DeleteFile(It.IsAny(), It.IsAny()), Times.Once()); } - [Test] - public void should_delete_the_same_track_file_only_once() - { - GivenMultipleTracksWithSingleTrackFile(); - - Subject.UpgradeTrackFile(_trackFile, _localTrack); - - Mocker.GetMock().Verify(v => v.DeleteFile(It.IsAny(), It.IsAny()), Times.Once()); - } - - [Test] - public void should_delete_multiple_different_track_files() - { - GivenMultipleTracksWithMultipleTrackFiles(); - - Subject.UpgradeTrackFile(_trackFile, _localTrack); - - Mocker.GetMock().Verify(v => v.DeleteFile(It.IsAny(), It.IsAny()), Times.Exactly(2)); - } - [Test] public void should_delete_track_file_from_database() { @@ -134,7 +83,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.UpgradeTrackFile(_trackFile, _localTrack); - Mocker.GetMock().Verify(v => v.Delete(It.IsAny(), DeleteMediaFileReason.Upgrade), Times.Once()); + Mocker.GetMock().Verify(v => v.Delete(It.IsAny(), DeleteMediaFileReason.Upgrade), Times.Once()); } [Test] @@ -148,7 +97,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.UpgradeTrackFile(_trackFile, _localTrack); - Mocker.GetMock().Verify(v => v.Delete(_localTrack.Tracks.Single().TrackFile.Value, DeleteMediaFileReason.Upgrade), Times.Once()); + // Mocker.GetMock().Verify(v => v.Delete(_localTrack.Album.BookFiles.Value, DeleteMediaFileReason.Upgrade), Times.Once()); } [Test] @@ -174,26 +123,16 @@ namespace NzbDrone.Core.Test.MediaFiles } [Test] - public void should_return_old_track_files_in_oldFiles() - { - GivenMultipleTracksWithMultipleTrackFiles(); - - Subject.UpgradeTrackFile(_trackFile, _localTrack).OldFiles.Count.Should().Be(2); - } - - [Test] + [Ignore("Pending readarr fix")] public void should_import_if_existing_file_doesnt_exist_in_db() { - _localTrack.Tracks = Builder.CreateListOfSize(1) - .All() - .With(e => e.TrackFileId = 1) - .With(e => e.TrackFile = new LazyLoaded(null)) - .Build() - .ToList(); + _localTrack.Album = Builder.CreateNew() + .With(e => e.BookFiles = new LazyLoaded>()) + .Build(); Subject.UpgradeTrackFile(_trackFile, _localTrack); - Mocker.GetMock().Verify(v => v.Delete(_localTrack.Tracks.Single().TrackFile.Value, It.IsAny()), Times.Never()); + // Mocker.GetMock().Verify(v => v.Delete(_localTrack.Album.BookFiles.Value, It.IsAny()), Times.Never()); } } } diff --git a/src/NzbDrone.Core.Test/Messaging/Commands/CommandEqualityComparerFixture.cs b/src/NzbDrone.Core.Test/Messaging/Commands/CommandEqualityComparerFixture.cs index 5982015a9..fbe7e8357 100644 --- a/src/NzbDrone.Core.Test/Messaging/Commands/CommandEqualityComparerFixture.cs +++ b/src/NzbDrone.Core.Test/Messaging/Commands/CommandEqualityComparerFixture.cs @@ -36,8 +36,8 @@ namespace NzbDrone.Core.Test.Messaging.Commands [Test] public void should_return_true_when_single_property_matches() { - var command1 = new AlbumSearchCommand { AlbumIds = new List { 1 } }; - var command2 = new AlbumSearchCommand { AlbumIds = new List { 1 } }; + var command1 = new AlbumSearchCommand { BookIds = new List { 1 } }; + var command2 = new AlbumSearchCommand { BookIds = new List { 1 } }; CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeTrue(); } @@ -45,8 +45,8 @@ namespace NzbDrone.Core.Test.Messaging.Commands [Test] public void should_return_false_when_single_property_doesnt_match() { - var command1 = new AlbumSearchCommand { AlbumIds = new List { 1 } }; - var command2 = new AlbumSearchCommand { AlbumIds = new List { 2 } }; + var command1 = new AlbumSearchCommand { BookIds = new List { 1 } }; + var command2 = new AlbumSearchCommand { BookIds = new List { 2 } }; CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeFalse(); } @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.Messaging.Commands public void should_return_false_when_only_one_has_properties() { var command1 = new ArtistSearchCommand(); - var command2 = new ArtistSearchCommand { ArtistId = 2 }; + var command2 = new ArtistSearchCommand { AuthorId = 2 }; CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeFalse(); } @@ -78,8 +78,8 @@ namespace NzbDrone.Core.Test.Messaging.Commands [Test] public void should_return_false_when_commands_list_are_different_lengths() { - var command1 = new AlbumSearchCommand { AlbumIds = new List { 1 } }; - var command2 = new AlbumSearchCommand { AlbumIds = new List { 1, 2 } }; + var command1 = new AlbumSearchCommand { BookIds = new List { 1 } }; + var command2 = new AlbumSearchCommand { BookIds = new List { 1, 2 } }; CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeFalse(); } @@ -87,8 +87,8 @@ namespace NzbDrone.Core.Test.Messaging.Commands [Test] public void should_return_false_when_commands_list_dont_match() { - var command1 = new AlbumSearchCommand { AlbumIds = new List { 1 } }; - var command2 = new AlbumSearchCommand { AlbumIds = new List { 2 } }; + var command1 = new AlbumSearchCommand { BookIds = new List { 1 } }; + var command2 = new AlbumSearchCommand { BookIds = new List { 2 } }; CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/MetadataSource/MetadataRequestBuilderFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/MetadataRequestBuilderFixture.cs index 996fd6425..5185bde6f 100644 --- a/src/NzbDrone.Core.Test/MetadataSource/MetadataRequestBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSource/MetadataRequestBuilderFixture.cs @@ -20,14 +20,14 @@ namespace NzbDrone.Core.Test.MetadataSource Mocker.GetMock() .Setup(s => s.Search) - .Returns(new HttpRequestBuilder("https://api.readarr.audio/api/v0.4/{route}").CreateFactory()); + .Returns(new HttpRequestBuilder("https://api.readarr.com/api/v0.4/{route}").CreateFactory()); } private void WithCustomProvider() { Mocker.GetMock() .Setup(s => s.MetadataSource) - .Returns("http://api.readarr.audio/api/testing/"); + .Returns("http://api.readarr.com/api/testing/"); } [TestCase] diff --git a/src/NzbDrone.Core.Test/MetadataSource/SearchArtistComparerFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SearchArtistComparerFixture.cs index ee432790e..6c697c70b 100644 --- a/src/NzbDrone.Core.Test/MetadataSource/SearchArtistComparerFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSource/SearchArtistComparerFixture.cs @@ -11,17 +11,17 @@ namespace NzbDrone.Core.Test.MetadataSource [TestFixture] public class SearchArtistComparerFixture : CoreTest { - private List _artist; + private List _artist; [SetUp] public void Setup() { - _artist = new List(); + _artist = new List(); } private void WithSeries(string name) { - _artist.Add(new Artist { Name = name }); + _artist.Add(new Author { Name = name }); } [Test] diff --git a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs index 5b818ff2e..ea4d4a2dd 100644 --- a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxyFixture.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; -using System.Linq; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MetadataSource.SkyHook; -using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.Music; using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.Test.Framework; @@ -23,33 +21,7 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook { UseRealHttp(); - _metadataProfile = new MetadataProfile - { - PrimaryAlbumTypes = new List - { - new ProfilePrimaryAlbumTypeItem - { - PrimaryAlbumType = PrimaryAlbumType.Album, - Allowed = true - } - }, - SecondaryAlbumTypes = new List - { - new ProfileSecondaryAlbumTypeItem() - { - SecondaryAlbumType = SecondaryAlbumType.Studio, - Allowed = true - } - }, - ReleaseStatuses = new List - { - new ProfileReleaseStatusItem - { - ReleaseStatus = ReleaseStatus.Official, - Allowed = true - } - } - }; + _metadataProfile = new MetadataProfile(); Mocker.GetMock() .Setup(s => s.Get(It.IsAny())) @@ -60,164 +32,61 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook .Returns(true); } - public List GivenExampleAlbums() - { - var result = new List(); - - foreach (var primaryType in PrimaryAlbumType.All) - { - foreach (var secondaryType in SecondaryAlbumType.All) - { - var secondaryTypes = secondaryType.Name == "Studio" ? new List() : new List { secondaryType.Name }; - foreach (var releaseStatus in ReleaseStatus.All) - { - var releaseStatuses = new List { releaseStatus.Name }; - result.Add(new AlbumResource - { - Type = primaryType.Name, - SecondaryTypes = secondaryTypes, - ReleaseStatuses = releaseStatuses - }); - } - } - } - - return result; - } - - [TestCase("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park")] - [TestCase("66c662b6-6e2f-4930-8610-912e24c63ed1", "AC/DC")] - public void should_be_able_to_get_artist_detail(string mbId, string name) + [TestCase("amzn1.gr.author.v1.qTrNu9-PIaaBj5gYRDmN4Q", "Terry Pratchett")] + [TestCase("amzn1.gr.author.v1.afCyJgprpWE2xJU2_z3zTQ", "Robert Harris")] + public void should_be_able_to_get_author_detail(string mbId, string name) { - var details = Subject.GetArtistInfo(mbId, 1); + var details = Subject.GetAuthorInfo(mbId); - ValidateArtist(details); - ValidateAlbums(details.Albums.Value, true); + ValidateAuthor(details); details.Name.Should().Be(name); } - [TestCaseSource(typeof(PrimaryAlbumType), "All")] - public void should_filter_albums_by_primary_release_type(PrimaryAlbumType type) - { - _metadataProfile.PrimaryAlbumTypes = new List - { - new ProfilePrimaryAlbumTypeItem - { - PrimaryAlbumType = type, - Allowed = true - } - }; - - var albums = GivenExampleAlbums(); - Subject.FilterAlbums(albums, 1).Select(x => x.Type).Distinct() - .Should().BeEquivalentTo(new List { type.Name }); - } - - [TestCaseSource(typeof(SecondaryAlbumType), "All")] - public void should_filter_albums_by_secondary_release_type(SecondaryAlbumType type) - { - _metadataProfile.SecondaryAlbumTypes = new List - { - new ProfileSecondaryAlbumTypeItem - { - SecondaryAlbumType = type, - Allowed = true - } - }; - - var albums = GivenExampleAlbums(); - var filtered = Subject.FilterAlbums(albums, 1); - TestLogger.Debug(filtered.Count()); - - filtered.SelectMany(x => x.SecondaryTypes.Select(SkyHookProxy.MapSecondaryTypes)) - .Select(x => x.Name) - .Distinct() - .Should().BeEquivalentTo(type.Name == "Studio" ? new List() : new List { type.Name }); - } - - [TestCaseSource(typeof(ReleaseStatus), "All")] - public void should_filter_albums_by_release_status(ReleaseStatus type) - { - _metadataProfile.ReleaseStatuses = new List - { - new ProfileReleaseStatusItem - { - ReleaseStatus = type, - Allowed = true - } - }; - - var albums = GivenExampleAlbums(); - Subject.FilterAlbums(albums, 1).SelectMany(x => x.ReleaseStatuses).Distinct() - .Should().BeEquivalentTo(new List { type.Name }); - } - - [TestCase("12fa3845-7c62-36e5-a8da-8be137155a72", "Hysteria")] - public void should_be_able_to_get_album_detail(string mbId, string name) - { - var details = Subject.GetAlbumInfo(mbId); - - ValidateAlbums(new List { details.Item2 }); - - details.Item2.Title.Should().Be(name); - } - - [TestCase("12fa3845-7c62-36e5-a8da-8be137155a72", "3c186b52-ca73-46a3-a8e6-04559bfbb581", 1, 13, "Hysteria")] - [TestCase("12fa3845-7c62-36e5-a8da-8be137155a72", "dee9ca6f-4f84-4359-82a9-b75a37ffc316", 2, 27, "Hysteria")] - public void should_be_able_to_get_album_detail_with_release(string mbId, string release, int mediaCount, int trackCount, string name) + [TestCase("amzn1.gr.book.v1.2rp8a0vJ8clGzMzZf61R9Q", "Guards! Guards!")] + public void should_be_able_to_get_book_detail(string mbId, string name) { - var details = Subject.GetAlbumInfo(mbId); + var details = Subject.GetBookInfo(mbId); - ValidateAlbums(new List { details.Item2 }); + ValidateAlbums(new List { details.Item2 }); - details.Item2.AlbumReleases.Value.Single(r => r.ForeignReleaseId == release).Media.Count.Should().Be(mediaCount); - details.Item2.AlbumReleases.Value.Single(r => r.ForeignReleaseId == release).Tracks.Value.Count.Should().Be(trackCount); details.Item2.Title.Should().Be(name); } [Test] public void getting_details_of_invalid_artist() { - Assert.Throws(() => Subject.GetArtistInfo("66c66aaa-6e2f-4930-8610-912e24c63ed1", 1)); - } - - [Test] - public void getting_details_of_invalid_guid_for_artist() - { - Assert.Throws(() => Subject.GetArtistInfo("66c66aaa-6e2f-4930-aaaaaa", 1)); + Assert.Throws(() => Subject.GetAuthorInfo("66c66aaa-6e2f-4930-8610-912e24c63ed1")); } [Test] public void getting_details_of_invalid_album() { - Assert.Throws(() => Subject.GetAlbumInfo("66c66aaa-6e2f-4930-8610-912e24c63ed1")); - } - - [Test] - public void getting_details_of_invalid_guid_for_album() - { - Assert.Throws(() => Subject.GetAlbumInfo("66c66aaa-6e2f-4930-aaaaaa")); + Assert.Throws(() => Subject.GetBookInfo("66c66aaa-6e2f-4930-8610-912e24c63ed1")); } - private void ValidateArtist(Artist artist) + private void ValidateAuthor(Author author) { - artist.Should().NotBeNull(); - artist.Name.Should().NotBeNullOrWhiteSpace(); - artist.CleanName.Should().Be(Parser.Parser.CleanArtistName(artist.Name)); - artist.SortName.Should().Be(Parser.Parser.NormalizeTitle(artist.Name)); - artist.Metadata.Value.Overview.Should().NotBeNullOrWhiteSpace(); - artist.Metadata.Value.Images.Should().NotBeEmpty(); - artist.ForeignArtistId.Should().NotBeNullOrWhiteSpace(); + author.Should().NotBeNull(); + author.Name.Should().NotBeNullOrWhiteSpace(); + author.CleanName.Should().Be(Parser.Parser.CleanArtistName(author.Name)); + author.SortName.Should().Be(Parser.Parser.NormalizeTitle(author.Name)); + author.Metadata.Value.TitleSlug.Should().NotBeNullOrWhiteSpace(); + author.Metadata.Value.Overview.Should().NotBeNullOrWhiteSpace(); + author.Metadata.Value.Images.Should().NotBeEmpty(); + author.ForeignAuthorId.Should().NotBeNullOrWhiteSpace(); + author.Books.IsLoaded.Should().BeTrue(); + author.Books.Value.Should().NotBeEmpty(); + author.Books.Value.Should().OnlyContain(x => x.CleanTitle != null); } - private void ValidateAlbums(List albums, bool idOnly = false) + private void ValidateAlbums(List albums, bool idOnly = false) { albums.Should().NotBeEmpty(); foreach (var album in albums) { - album.ForeignAlbumId.Should().NotBeNullOrWhiteSpace(); + album.ForeignBookId.Should().NotBeNullOrWhiteSpace(); if (!idOnly) { ValidateAlbum(album); @@ -231,12 +100,11 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook } } - private void ValidateAlbum(Album album) + private void ValidateAlbum(Book album) { album.Should().NotBeNull(); album.Title.Should().NotBeNullOrWhiteSpace(); - album.AlbumType.Should().NotBeNullOrWhiteSpace(); album.Should().NotBeNull(); diff --git a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs index b8535aefc..720f479f9 100644 --- a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs @@ -19,34 +19,7 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook { UseRealHttp(); - var metadataProfile = new MetadataProfile - { - Id = 1, - PrimaryAlbumTypes = new List - { - new ProfilePrimaryAlbumTypeItem - { - PrimaryAlbumType = PrimaryAlbumType.Album, - Allowed = true - } - }, - SecondaryAlbumTypes = new List - { - new ProfileSecondaryAlbumTypeItem() - { - SecondaryAlbumType = SecondaryAlbumType.Studio, - Allowed = true - } - }, - ReleaseStatuses = new List - { - new ProfileReleaseStatusItem - { - ReleaseStatus = ReleaseStatus.Official, - Allowed = true - } - } - }; + var metadataProfile = new MetadataProfile(); Mocker.GetMock() .Setup(s => s.All()) @@ -57,16 +30,12 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook .Returns(metadataProfile); } - [TestCase("Coldplay", "Coldplay")] - [TestCase("Avenged Sevenfold", "Avenged Sevenfold")] - [TestCase("3OH!3", "3OH!3")] - [TestCase("The Academy Is...", "The Academy Is…")] - [TestCase("readarr:f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park")] - [TestCase("readarrid:f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park")] - [TestCase("readarrid: f59c5520-5f46-4d2c-b2c4-822eabf53419 ", "Linkin Park")] + [TestCase("Robert Harris", "Robert Harris")] + [TestCase("Terry Pratchett", "Terry Pratchett")] + [TestCase("Charlotte Brontë", "Charlotte Brontë")] public void successful_artist_search(string title, string expected) { - var result = Subject.SearchForNewArtist(title); + var result = Subject.SearchForNewAuthor(title); result.Should().NotBeEmpty(); @@ -75,14 +44,16 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook ExceptionVerification.IgnoreWarns(); } - [TestCase("Evolve", "Imagine Dragons", "Evolve")] - [TestCase("Hysteria", null, "Hysteria")] - [TestCase("readarr:d77df681-b779-3d6d-b66a-3bfd15985e3e", null, "Pyromania")] - [TestCase("readarr: d77df681-b779-3d6d-b66a-3bfd15985e3e", null, "Pyromania")] - [TestCase("readarrid:d77df681-b779-3d6d-b66a-3bfd15985e3e", null, "Pyromania")] + [TestCase("Harry Potter and the sorcerer's stone", null, "Harry Potter and the Sorcerer's Stone")] + [TestCase("readarr:3", null, "Harry Potter and the Sorcerer's Stone")] + [TestCase("readarr: 3", null, "Harry Potter and the Sorcerer's Stone")] + [TestCase("readarrid:3", null, "Harry Potter and the Sorcerer's Stone")] + [TestCase("goodreads:3", null, "Harry Potter and the Sorcerer's Stone")] + [TestCase("asin:B0192CTMYG", null, "Harry Potter and the Sorcerer's Stone")] + [TestCase("isbn:9780439554930", null, "Harry Potter and the Sorcerer's Stone")] public void successful_album_search(string title, string artist, string expected) { - var result = Subject.SearchForNewAlbum(title, artist); + var result = Subject.SearchForNewBook(title, artist); result.Should().NotBeEmpty(); @@ -95,34 +66,33 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook [TestCase("readarrid: 99999999999999999999")] [TestCase("readarrid: 0")] [TestCase("readarrid: -12")] - [TestCase("readarrid:289578")] + [TestCase("readarrid: aaaa")] [TestCase("adjalkwdjkalwdjklawjdlKAJD")] public void no_artist_search_result(string term) { - var result = Subject.SearchForNewArtist(term); + var result = Subject.SearchForNewAuthor(term); result.Should().BeEmpty(); ExceptionVerification.IgnoreWarns(); } - [TestCase("Eminem", 0, typeof(Artist), "Eminem")] - [TestCase("Eminem Kamikaze", 0, typeof(Artist), "Eminem")] - [TestCase("Eminem Kamikaze", 1, typeof(Album), "Kamikaze")] + [TestCase("Robert Harris", 0, typeof(Author), "Robert Harris")] + [TestCase("Robert Harris", 1, typeof(Book), "Fatherland")] public void successful_combined_search(string query, int position, Type resultType, string expected) { var result = Subject.SearchForNewEntity(query); result.Should().NotBeEmpty(); result[position].GetType().Should().Be(resultType); - if (resultType == typeof(Artist)) + if (resultType == typeof(Author)) { - var cast = result[position] as Artist; + var cast = result[position] as Author; cast.Should().NotBeNull(); cast.Name.Should().Be(expected); } else { - var cast = result[position] as Album; + var cast = result[position] as Book; cast.Should().NotBeNull(); cast.Title.Should().Be(expected); } diff --git a/src/NzbDrone.Core.Test/MusicTests/AddAlbumFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AddAlbumFixture.cs index 2308629d8..87629b9fc 100644 --- a/src/NzbDrone.Core.Test/MusicTests/AddAlbumFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/AddAlbumFixture.cs @@ -17,51 +17,51 @@ namespace NzbDrone.Core.Test.MusicTests [TestFixture] public class AddAlbumFixture : CoreTest { - private Artist _fakeArtist; - private Album _fakeAlbum; + private Author _fakeArtist; + private Book _fakeAlbum; [SetUp] public void Setup() { - _fakeAlbum = Builder + _fakeAlbum = Builder .CreateNew() .Build(); - _fakeArtist = Builder + _fakeArtist = Builder .CreateNew() .With(s => s.Path = null) - .With(s => s.Metadata = Builder.CreateNew().Build()) + .With(s => s.Metadata = Builder.CreateNew().Build()) .Build(); } private void GivenValidAlbum(string readarrId) { - Mocker.GetMock() - .Setup(s => s.GetAlbumInfo(readarrId)) - .Returns(Tuple.Create(_fakeArtist.Metadata.Value.ForeignArtistId, + Mocker.GetMock() + .Setup(s => s.GetBookInfo(readarrId)) + .Returns(Tuple.Create(_fakeArtist.Metadata.Value.ForeignAuthorId, _fakeAlbum, - new List { _fakeArtist.Metadata.Value })); + new List { _fakeArtist.Metadata.Value })); Mocker.GetMock() - .Setup(s => s.AddArtist(It.IsAny(), It.IsAny())) + .Setup(s => s.AddArtist(It.IsAny(), It.IsAny())) .Returns(_fakeArtist); } private void GivenValidPath() { Mocker.GetMock() - .Setup(s => s.GetArtistFolder(It.IsAny(), null)) - .Returns((c, n) => c.Name); + .Setup(s => s.GetArtistFolder(It.IsAny(), null)) + .Returns((c, n) => c.Name); } - private Album AlbumToAdd(string albumId, string artistId) + private Book AlbumToAdd(string bookId, string authorId) { - return new Album + return new Book { - ForeignAlbumId = albumId, - ArtistMetadata = new ArtistMetadata + ForeignBookId = bookId, + AuthorMetadata = new AuthorMetadata { - ForeignArtistId = artistId + ForeignAuthorId = authorId } }; } @@ -71,7 +71,7 @@ namespace NzbDrone.Core.Test.MusicTests { var newAlbum = AlbumToAdd("5537624c-3d2f-4f5c-8099-df916082c85c", "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493"); - GivenValidAlbum(newAlbum.ForeignAlbumId); + GivenValidAlbum(newAlbum.ForeignBookId); GivenValidPath(); var album = Subject.AddAlbum(newAlbum); @@ -84,9 +84,9 @@ namespace NzbDrone.Core.Test.MusicTests { var newAlbum = AlbumToAdd("5537624c-3d2f-4f5c-8099-df916082c85c", "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493"); - Mocker.GetMock() - .Setup(s => s.GetAlbumInfo(newAlbum.ForeignAlbumId)) - .Throws(new AlbumNotFoundException(newAlbum.ForeignAlbumId)); + Mocker.GetMock() + .Setup(s => s.GetBookInfo(newAlbum.ForeignBookId)) + .Throws(new AlbumNotFoundException(newAlbum.ForeignBookId)); Assert.Throws(() => Subject.AddAlbum(newAlbum)); diff --git a/src/NzbDrone.Core.Test/MusicTests/AddArtistFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AddArtistFixture.cs index 9c08f31a0..1a8f60977 100644 --- a/src/NzbDrone.Core.Test/MusicTests/AddArtistFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/AddArtistFixture.cs @@ -18,46 +18,46 @@ namespace NzbDrone.Core.Test.MusicTests [TestFixture] public class AddArtistFixture : CoreTest { - private Artist _fakeArtist; + private Author _fakeArtist; [SetUp] public void Setup() { - _fakeArtist = Builder + _fakeArtist = Builder .CreateNew() .With(s => s.Path = null) .Build(); - _fakeArtist.Albums = new List(); + _fakeArtist.Books = new List(); } private void GivenValidArtist(string readarrId) { - Mocker.GetMock() - .Setup(s => s.GetArtistInfo(readarrId, It.IsAny())) + Mocker.GetMock() + .Setup(s => s.GetAuthorInfo(readarrId)) .Returns(_fakeArtist); } private void GivenValidPath() { Mocker.GetMock() - .Setup(s => s.GetArtistFolder(It.IsAny(), null)) - .Returns((c, n) => c.Name); + .Setup(s => s.GetArtistFolder(It.IsAny(), null)) + .Returns((c, n) => c.Name); Mocker.GetMock() - .Setup(s => s.Validate(It.IsAny())) + .Setup(s => s.Validate(It.IsAny())) .Returns(new ValidationResult()); } [Test] public void should_be_able_to_add_a_artist_without_passing_in_name() { - var newArtist = new Artist + var newArtist = new Author { - ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", + ForeignAuthorId = "ce09ea31-3d4a-4487-a797-e315175457a0", RootFolderPath = @"C:\Test\Music" }; - GivenValidArtist(newArtist.ForeignArtistId); + GivenValidArtist(newArtist.ForeignAuthorId); GivenValidPath(); var artist = Subject.AddArtist(newArtist); @@ -68,13 +68,13 @@ namespace NzbDrone.Core.Test.MusicTests [Test] public void should_have_proper_path() { - var newArtist = new Artist + var newArtist = new Author { - ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", + ForeignAuthorId = "ce09ea31-3d4a-4487-a797-e315175457a0", RootFolderPath = @"C:\Test\Music" }; - GivenValidArtist(newArtist.ForeignArtistId); + GivenValidArtist(newArtist.ForeignAuthorId); GivenValidPath(); var artist = Subject.AddArtist(newArtist); @@ -85,16 +85,16 @@ namespace NzbDrone.Core.Test.MusicTests [Test] public void should_throw_if_artist_validation_fails() { - var newArtist = new Artist + var newArtist = new Author { - ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", + ForeignAuthorId = "ce09ea31-3d4a-4487-a797-e315175457a0", Path = @"C:\Test\Music\Name1" }; - GivenValidArtist(newArtist.ForeignArtistId); + GivenValidArtist(newArtist.ForeignAuthorId); Mocker.GetMock() - .Setup(s => s.Validate(It.IsAny())) + .Setup(s => s.Validate(It.IsAny())) .Returns(new ValidationResult(new List { new ValidationFailure("Path", "Test validation failure") @@ -106,18 +106,18 @@ namespace NzbDrone.Core.Test.MusicTests [Test] public void should_throw_if_artist_cannot_be_found() { - var newArtist = new Artist + var newArtist = new Author { - ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", + ForeignAuthorId = "ce09ea31-3d4a-4487-a797-e315175457a0", Path = @"C:\Test\Music\Name1" }; - Mocker.GetMock() - .Setup(s => s.GetArtistInfo(newArtist.ForeignArtistId, newArtist.MetadataProfileId)) - .Throws(new ArtistNotFoundException(newArtist.ForeignArtistId)); + Mocker.GetMock() + .Setup(s => s.GetAuthorInfo(newArtist.ForeignAuthorId)) + .Throws(new ArtistNotFoundException(newArtist.ForeignAuthorId)); Mocker.GetMock() - .Setup(s => s.Validate(It.IsAny())) + .Setup(s => s.Validate(It.IsAny())) .Returns(new ValidationResult(new List { new ValidationFailure("Path", "Test validation failure") @@ -131,15 +131,15 @@ namespace NzbDrone.Core.Test.MusicTests [Test] public void should_disambiguate_if_artist_folder_exists() { - var newArtist = new Artist + var newArtist = new Author { - ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", + ForeignAuthorId = "ce09ea31-3d4a-4487-a797-e315175457a0", Path = @"C:\Test\Music\Name1", }; - _fakeArtist.Metadata = Builder.CreateNew().With(x => x.Disambiguation = "Disambiguation").Build(); + _fakeArtist.Metadata = Builder.CreateNew().With(x => x.Disambiguation = "Disambiguation").Build(); - GivenValidArtist(newArtist.ForeignArtistId); + GivenValidArtist(newArtist.ForeignAuthorId); GivenValidPath(); Mocker.GetMock() @@ -153,15 +153,15 @@ namespace NzbDrone.Core.Test.MusicTests [Test] public void should_disambiguate_with_numbers_if_artist_folder_still_exists() { - var newArtist = new Artist + var newArtist = new Author { - ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", + ForeignAuthorId = "ce09ea31-3d4a-4487-a797-e315175457a0", Path = @"C:\Test\Music\Name1", }; - _fakeArtist.Metadata = Builder.CreateNew().With(x => x.Disambiguation = "Disambiguation").Build(); + _fakeArtist.Metadata = Builder.CreateNew().With(x => x.Disambiguation = "Disambiguation").Build(); - GivenValidArtist(newArtist.ForeignArtistId); + GivenValidArtist(newArtist.ForeignAuthorId); GivenValidPath(); Mocker.GetMock() @@ -187,15 +187,15 @@ namespace NzbDrone.Core.Test.MusicTests [Test] public void should_disambiguate_with_numbers_if_artist_folder_exists_and_no_disambiguation() { - var newArtist = new Artist + var newArtist = new Author { - ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0", + ForeignAuthorId = "ce09ea31-3d4a-4487-a797-e315175457a0", Path = @"C:\Test\Music\Name1", }; - _fakeArtist.Metadata = Builder.CreateNew().With(x => x.Disambiguation = string.Empty).Build(); + _fakeArtist.Metadata = Builder.CreateNew().With(x => x.Disambiguation = string.Empty).Build(); - GivenValidArtist(newArtist.ForeignArtistId); + GivenValidArtist(newArtist.ForeignAuthorId); GivenValidPath(); Mocker.GetMock() diff --git a/src/NzbDrone.Core.Test/MusicTests/AlbumMonitoredServiceTests/AlbumMonitoredServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AlbumMonitoredServiceTests/AlbumMonitoredServiceFixture.cs index 641b87f7b..44438e2a4 100644 --- a/src/NzbDrone.Core.Test/MusicTests/AlbumMonitoredServiceTests/AlbumMonitoredServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/AlbumMonitoredServiceTests/AlbumMonitoredServiceFixture.cs @@ -13,18 +13,18 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumMonitoredServiceTests [TestFixture] public class SetAlbumMontitoredFixture : CoreTest { - private Artist _artist; - private List _albums; + private Author _artist; + private List _albums; [SetUp] public void Setup() { const int albums = 4; - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .Build(); - _albums = Builder.CreateListOfSize(albums) + _albums = Builder.CreateListOfSize(albums) .All() .With(e => e.Monitored = true) .With(e => e.ReleaseDate = DateTime.UtcNow.AddDays(-7)) @@ -44,12 +44,8 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumMonitoredServiceTests .Returns(_albums); Mocker.GetMock() - .Setup(s => s.GetArtistAlbumsWithFiles(It.IsAny())) - .Returns(new List()); - - Mocker.GetMock() - .Setup(s => s.GetTracksByAlbum(It.IsAny())) - .Returns(new List()); + .Setup(s => s.GetArtistAlbumsWithFiles(It.IsAny())) + .Returns(new List()); } [Test] @@ -58,24 +54,24 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumMonitoredServiceTests Subject.SetAlbumMonitoredStatus(_artist, null); Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.IsAny()), Times.Once()); + .Verify(v => v.UpdateArtist(It.IsAny()), Times.Once()); Mocker.GetMock() - .Verify(v => v.UpdateMany(It.IsAny>()), Times.Never()); + .Verify(v => v.UpdateMany(It.IsAny>()), Times.Never()); } [Test] public void should_be_able_to_monitor_albums_when_passed_in_artist() { - var albumsToMonitor = new List { _albums.First().ForeignAlbumId }; + var albumsToMonitor = new List { _albums.First().ForeignBookId }; Subject.SetAlbumMonitoredStatus(_artist, new MonitoringOptions { Monitored = true, AlbumsToMonitor = albumsToMonitor }); Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.IsAny()), Times.Once()); + .Verify(v => v.UpdateArtist(It.IsAny()), Times.Once()); - VerifyMonitored(e => e.ForeignAlbumId == _albums.First().ForeignAlbumId); - VerifyNotMonitored(e => e.ForeignAlbumId != _albums.First().ForeignAlbumId); + VerifyMonitored(e => e.ForeignBookId == _albums.First().ForeignBookId); + VerifyNotMonitored(e => e.ForeignBookId != _albums.First().ForeignBookId); } [Test] @@ -84,7 +80,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumMonitoredServiceTests Subject.SetAlbumMonitoredStatus(_artist, new MonitoringOptions { Monitor = MonitorTypes.All }); Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(l => l.All(e => e.Monitored)))); + .Verify(v => v.UpdateMany(It.Is>(l => l.All(e => e.Monitored)))); } [Test] @@ -102,16 +98,16 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumMonitoredServiceTests VerifyNotMonitored(e => e.ReleaseDate.HasValue && e.ReleaseDate.Value.Before(DateTime.UtcNow)); } - private void VerifyMonitored(Func predicate) + private void VerifyMonitored(Func predicate) { Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(l => l.Where(predicate).All(e => e.Monitored)))); + .Verify(v => v.UpdateMany(It.Is>(l => l.Where(predicate).All(e => e.Monitored)))); } - private void VerifyNotMonitored(Func predicate) + private void VerifyNotMonitored(Func predicate) { Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(l => l.Where(predicate).All(e => !e.Monitored)))); + .Verify(v => v.UpdateMany(It.Is>(l => l.Where(predicate).All(e => !e.Monitored)))); } } } diff --git a/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs index c6bc5cbb9..c47bd068d 100644 --- a/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs @@ -10,91 +10,62 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests { [TestFixture] - public class AlbumRepositoryFixture : DbTest + public class AlbumRepositoryFixture : DbTest { - private Artist _artist; - private Album _album; - private Album _albumSpecial; - private List _albums; - private AlbumRelease _release; + private Author _artist; + private Book _album; + private Book _albumSpecial; + private List _albums; private AlbumRepository _albumRepo; - private ReleaseRepository _releaseRepo; [SetUp] public void Setup() { - _artist = new Artist + _artist = new Author { Name = "Alien Ant Farm", Monitored = true, - ForeignArtistId = "this is a fake id", + ForeignAuthorId = "this is a fake id", Id = 1, - ArtistMetadataId = 1 + AuthorMetadataId = 1 }; _albumRepo = Mocker.Resolve(); - _releaseRepo = Mocker.Resolve(); - _release = Builder - .CreateNew() - .With(e => e.Id = 0) - .With(e => e.ForeignReleaseId = "e00e40a3-5ed5-4ed3-9c22-0a8ff4119bdf") - .With(e => e.Monitored = true) - .Build(); - - _album = new Album + _album = new Book { Title = "ANThology", - ForeignAlbumId = "1", + ForeignBookId = "1", + ForeignWorkId = "1", + TitleSlug = "1-ANThology", CleanTitle = "anthology", - Artist = _artist, - ArtistMetadataId = _artist.ArtistMetadataId, - AlbumType = "", - AlbumReleases = new List { _release }, + Author = _artist, + AuthorMetadataId = _artist.AuthorMetadataId, }; _albumRepo.Insert(_album); - _release.AlbumId = _album.Id; - _releaseRepo.Insert(_release); _albumRepo.Update(_album); - _albumSpecial = new Album + _albumSpecial = new Book { Title = "+", - ForeignAlbumId = "2", + ForeignBookId = "2", + ForeignWorkId = "2", + TitleSlug = "2-_", CleanTitle = "", - Artist = _artist, - ArtistMetadataId = _artist.ArtistMetadataId, - AlbumType = "", - AlbumReleases = new List - { - new AlbumRelease - { - ForeignReleaseId = "fake id" - } - } + Author = _artist, + AuthorMetadataId = _artist.AuthorMetadataId }; _albumRepo.Insert(_albumSpecial); } - [Test] - public void should_find_album_in_db_by_releaseid() - { - var id = "e00e40a3-5ed5-4ed3-9c22-0a8ff4119bdf"; - - var album = _albumRepo.FindAlbumByRelease(id); - - album.Should().NotBeNull(); - album.Title.Should().Be(_album.Title); - } - [TestCase("ANThology")] [TestCase("anthology")] [TestCase("anthology!")] public void should_find_album_in_db_by_title(string title) { - var album = _albumRepo.FindByTitle(_artist.ArtistMetadataId, title); + var album = _albumRepo.FindByTitle(_artist.AuthorMetadataId, title); album.Should().NotBeNull(); album.Title.Should().Be(_album.Title); @@ -103,7 +74,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests [Test] public void should_find_album_in_db_by_title_all_special_characters() { - var album = _albumRepo.FindByTitle(_artist.ArtistMetadataId, "+"); + var album = _albumRepo.FindByTitle(_artist.AuthorMetadataId, "+"); album.Should().NotBeNull(); album.Title.Should().Be(_albumSpecial.Title); @@ -115,7 +86,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests [TestCase("÷")] public void should_not_find_album_in_db_by_incorrect_title(string title) { - var album = _albumRepo.FindByTitle(_artist.ArtistMetadataId, title); + var album = _albumRepo.FindByTitle(_artist.AuthorMetadataId, title); album.Should().BeNull(); } @@ -123,40 +94,30 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests [Test] public void should_not_find_album_when_two_albums_have_same_name() { - var albums = Builder.CreateListOfSize(2) + var albums = Builder.CreateListOfSize(2) .All() .With(x => x.Id = 0) - .With(x => x.Artist = _artist) - .With(x => x.ArtistMetadataId = _artist.ArtistMetadataId) + .With(x => x.Author = _artist) + .With(x => x.AuthorMetadataId = _artist.AuthorMetadataId) .With(x => x.Title = "Weezer") .With(x => x.CleanTitle = "weezer") .Build(); _albumRepo.InsertMany(albums); - var album = _albumRepo.FindByTitle(_artist.ArtistMetadataId, "Weezer"); + var album = _albumRepo.FindByTitle(_artist.AuthorMetadataId, "Weezer"); _albumRepo.All().Should().HaveCount(4); album.Should().BeNull(); } - [Test] - public void should_not_find_album_in_db_by_partial_releaseid() - { - var id = "e00e40a3-5ed5-4ed3-9c22"; - - var album = _albumRepo.FindAlbumByRelease(id); - - album.Should().BeNull(); - } - private void GivenMultipleAlbums() { - _albums = Builder.CreateListOfSize(4) + _albums = Builder.CreateListOfSize(4) .All() .With(x => x.Id = 0) - .With(x => x.Artist = _artist) - .With(x => x.ArtistMetadataId = _artist.ArtistMetadataId) + .With(x => x.Author = _artist) + .With(x => x.AuthorMetadataId = _artist.AuthorMetadataId) .TheFirst(1) // next @@ -183,7 +144,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests { GivenMultipleAlbums(); - var result = _albumRepo.GetNextAlbums(new[] { _artist.ArtistMetadataId }); + var result = _albumRepo.GetNextAlbums(new[] { _artist.AuthorMetadataId }); result.Should().BeEquivalentTo(_albums.Take(1)); } @@ -192,7 +153,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests { GivenMultipleAlbums(); - var result = _albumRepo.GetLastAlbums(new[] { _artist.ArtistMetadataId }); + var result = _albumRepo.GetLastAlbums(new[] { _artist.AuthorMetadataId }); result.Should().BeEquivalentTo(_albums.Skip(2).Take(1)); } } diff --git a/src/NzbDrone.Core.Test/MusicTests/AlbumServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AlbumServiceFixture.cs index 6faa5ebd1..b4110059a 100644 --- a/src/NzbDrone.Core.Test/MusicTests/AlbumServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/AlbumServiceFixture.cs @@ -10,19 +10,19 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests [TestFixture] public class AlbumServiceFixture : CoreTest { - private List _albums; + private List _albums; [SetUp] public void Setup() { - _albums = new List(); - _albums.Add(new Album + _albums = new List(); + _albums.Add(new Book { Title = "ANThology", CleanTitle = "anthology", }); - _albums.Add(new Album + _albums.Add(new Book { Title = "+", CleanTitle = "", @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests private void GivenSimilarAlbum() { - _albums.Add(new Album + _albums.Add(new Book { Title = "ANThology2", CleanTitle = "anthology2", diff --git a/src/NzbDrone.Core.Test/MusicTests/ArtistMetadataRepositoryTests/ArtistMetadataRepositoryFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ArtistMetadataRepositoryTests/ArtistMetadataRepositoryFixture.cs index 0fe2cdcd7..c562f0069 100644 --- a/src/NzbDrone.Core.Test/MusicTests/ArtistMetadataRepositoryTests/ArtistMetadataRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/ArtistMetadataRepositoryTests/ArtistMetadataRepositoryFixture.cs @@ -11,16 +11,16 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests { [TestFixture] - public class ArtistMetadataRepositoryFixture : DbTest + public class ArtistMetadataRepositoryFixture : DbTest { private ArtistMetadataRepository _artistMetadataRepo; - private List _metadataList; + private List _metadataList; [SetUp] public void Setup() { _artistMetadataRepo = Mocker.Resolve(); - _metadataList = Builder.CreateListOfSize(10).All().With(x => x.Id = 0).BuildList(); + _metadataList = Builder.CreateListOfSize(10).All().With(x => x.Id = 0).BuildList(); } [Test] diff --git a/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs index 48b5530cf..b9f71bcec 100644 --- a/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests { [TestFixture] - public class ArtistRepositoryFixture : DbTest + public class ArtistRepositoryFixture : DbTest { private ArtistRepository _artistRepo; private ArtistMetadataRepository _artistMetadataRepo; @@ -34,21 +34,21 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests oldIds = new List(); } - var metadata = Builder.CreateNew() + var metadata = Builder.CreateNew() .With(a => a.Id = 0) .With(a => a.Name = name) - .With(a => a.OldForeignArtistIds = oldIds) + .With(a => a.TitleSlug = foreignId) .BuildNew(); - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .With(a => a.Id = 0) .With(a => a.Metadata = metadata) .With(a => a.CleanName = Parser.Parser.CleanArtistName(name)) - .With(a => a.ForeignArtistId = foreignId) + .With(a => a.ForeignAuthorId = foreignId) .BuildNew(); _artistMetadataRepo.Insert(metadata); - artist.ArtistMetadataId = metadata.Id; + artist.AuthorMetadataId = metadata.Id; _artistRepo.Insert(artist); } @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests { var profile = new QualityProfile { - Items = Qualities.QualityFixture.GetDefaultQualities(Quality.FLAC, Quality.MP3_192, Quality.MP3_320), + Items = Qualities.QualityFixture.GetDefaultQualities(Quality.FLAC, Quality.MP3_320, Quality.MP3_320), Cutoff = Quality.FLAC.Id, Name = "TestProfile" @@ -71,16 +71,13 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests var metaProfile = new MetadataProfile { - Name = "TestProfile", - PrimaryAlbumTypes = new List(), - SecondaryAlbumTypes = new List(), - ReleaseStatuses = new List() + Name = "TestProfile" }; Mocker.Resolve().Insert(profile); Mocker.Resolve().Insert(metaProfile); - var artist = Builder.CreateNew().BuildNew(); + var artist = Builder.CreateNew().BuildNew(); artist.QualityProfileId = profile.Id; artist.MetadataProfileId = metaProfile.Id; @@ -108,18 +105,7 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests var artist = _artistRepo.FindById("d5be5333-4171-427e-8e12-732087c6b78e"); artist.Should().NotBeNull(); - artist.ForeignArtistId.Should().Be("d5be5333-4171-427e-8e12-732087c6b78e"); - } - - [Test] - public void should_find_artist_in_by_old_id() - { - GivenArtists(); - var artist = _artistRepo.FindById("6f2ed437-825c-4cea-bb58-bf7688c6317a"); - - artist.Should().NotBeNull(); - artist.Name.Should().Be("The Black Keys"); - artist.ForeignArtistId.Should().Be("d15721d8-56b4-453d-b506-fc915b14cba2"); + artist.ForeignAuthorId.Should().Be("d5be5333-4171-427e-8e12-732087c6b78e"); } [Test] @@ -141,12 +127,12 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests public void should_throw_sql_exception_adding_duplicate_artist() { var name = "test"; - var metadata = Builder.CreateNew() + var metadata = Builder.CreateNew() .With(a => a.Id = 0) .With(a => a.Name = name) .BuildNew(); - var artist1 = Builder.CreateNew() + var artist1 = Builder.CreateNew() .With(a => a.Id = 0) .With(a => a.Metadata = metadata) .With(a => a.CleanName = Parser.Parser.CleanArtistName(name)) diff --git a/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/FindByNameInexactFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/FindByNameInexactFixture.cs index 8e5b3e4e9..072d38394 100644 --- a/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/FindByNameInexactFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/FindByNameInexactFixture.cs @@ -11,21 +11,21 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistServiceTests public class FindByNameInexactFixture : CoreTest { - private List _artists; + private List _artists; - private Artist CreateArtist(string name) + private Author CreateArtist(string name) { - return Builder.CreateNew() + return Builder.CreateNew() .With(a => a.Name = name) .With(a => a.CleanName = Parser.Parser.CleanArtistName(name)) - .With(a => a.ForeignArtistId = name) + .With(a => a.ForeignAuthorId = name) .BuildNew(); } [SetUp] public void Setup() { - _artists = new List(); + _artists = new List(); _artists.Add(CreateArtist("The Black Eyed Peas")); _artists.Add(CreateArtist("The Black Keys")); @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistServiceTests [Test] public void should_find_artist_when_the_is_omitted_from_start() { - _artists = new List(); + _artists = new List(); _artists.Add(CreateArtist("Black Keys")); _artists.Add(CreateArtist("The Black Eyed Peas")); diff --git a/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/UpdateMultipleArtistFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/UpdateMultipleArtistFixture.cs index e9e77e2c2..c3c4da498 100644 --- a/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/UpdateMultipleArtistFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/ArtistServiceTests/UpdateMultipleArtistFixture.cs @@ -15,12 +15,12 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistServiceTests [TestFixture] public class UpdateMultipleArtistFixture : CoreTest { - private List _artists; + private List _artists; [SetUp] public void Setup() { - _artists = Builder.CreateListOfSize(5) + _artists = Builder.CreateListOfSize(5) .All() .With(s => s.QualityProfileId = 1) .With(s => s.Monitored) @@ -41,15 +41,15 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistServiceTests public void should_update_path_when_rootFolderPath_is_supplied() { Mocker.GetMock() - .Setup(s => s.GetArtistFolder(It.IsAny(), null)) - .Returns((c, n) => c.Name); + .Setup(s => s.GetArtistFolder(It.IsAny(), null)) + .Returns((c, n) => c.Name); var newRoot = @"C:\Test\Music2".AsOsAgnostic(); _artists.ForEach(s => s.RootFolderPath = newRoot); Mocker.GetMock() - .Setup(s => s.BuildPath(It.IsAny(), false)) - .Returns((s, u) => Path.Combine(s.RootFolderPath, s.Name)); + .Setup(s => s.BuildPath(It.IsAny(), false)) + .Returns((s, u) => Path.Combine(s.RootFolderPath, s.Name)); Subject.UpdateArtists(_artists, false).ForEach(s => s.Path.Should().StartWith(newRoot)); } @@ -67,15 +67,15 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistServiceTests [Test] public void should_be_able_to_update_many_artist() { - var artist = Builder.CreateListOfSize(50) + var artist = Builder.CreateListOfSize(50) .All() .With(s => s.Path = (@"C:\Test\Music\" + s.Path).AsOsAgnostic()) .Build() .ToList(); Mocker.GetMock() - .Setup(s => s.GetArtistFolder(It.IsAny(), null)) - .Returns((c, n) => c.Name); + .Setup(s => s.GetArtistFolder(It.IsAny(), null)) + .Returns((c, n) => c.Name); var newRoot = @"C:\Test\Music2".AsOsAgnostic(); artist.ForEach(s => s.RootFolderPath = newRoot); diff --git a/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs b/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs index a7d0dbbbf..bdc02a4af 100644 --- a/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.MusicTests [Test] public void two_equivalent_artist_metadata_should_be_equal() { - var item1 = _fixture.Create(); + var item1 = _fixture.Create(); var item2 = item1.JsonClone(); item1.Should().NotBeSameAs(item2); @@ -61,12 +61,12 @@ namespace NzbDrone.Core.Test.MusicTests } [Test] - [TestCaseSource(typeof(EqualityPropertySource), "TestCases")] + [TestCaseSource(typeof(EqualityPropertySource), "TestCases")] public void two_different_artist_metadata_should_not_be_equal(PropertyInfo prop) { - var item1 = _fixture.Create(); + var item1 = _fixture.Create(); var item2 = item1.JsonClone(); - var different = _fixture.Create(); + var different = _fixture.Create(); // make item2 different in the property under consideration var differentEntry = prop.GetValue(different); @@ -79,8 +79,8 @@ namespace NzbDrone.Core.Test.MusicTests [Test] public void metadata_and_db_fields_should_replicate_artist_metadata() { - var item1 = _fixture.Create(); - var item2 = _fixture.Create(); + var item1 = _fixture.Create(); + var item2 = _fixture.Create(); item1.Should().NotBe(item2); @@ -89,111 +89,12 @@ namespace NzbDrone.Core.Test.MusicTests item1.Should().Be(item2); } - private Track GivenTrack() + private Book GivenAlbum() { - return _fixture.Build() - .Without(x => x.AlbumRelease) - .Without(x => x.ArtistMetadata) - .Without(x => x.TrackFile) - .Without(x => x.Artist) - .Without(x => x.AlbumId) - .Without(x => x.Album) - .Create(); - } - - [Test] - public void two_equivalent_track_should_be_equal() - { - var item1 = GivenTrack(); - var item2 = item1.JsonClone(); - - item1.Should().NotBeSameAs(item2); - item1.Should().Be(item2); - } - - [Test] - [TestCaseSource(typeof(EqualityPropertySource), "TestCases")] - public void two_different_tracks_should_not_be_equal(PropertyInfo prop) - { - var item1 = GivenTrack(); - var item2 = item1.JsonClone(); - var different = GivenTrack(); - - // make item2 different in the property under consideration - var differentEntry = prop.GetValue(different); - prop.SetValue(item2, differentEntry); - - item1.Should().NotBeSameAs(item2); - item1.Should().NotBe(item2); - } - - [Test] - public void metadata_and_db_fields_should_replicate_track() - { - var item1 = GivenTrack(); - var item2 = GivenTrack(); - - item1.Should().NotBe(item2); - - item1.UseMetadataFrom(item2); - item1.UseDbFieldsFrom(item2); - item1.Should().Be(item2); - } - - private AlbumRelease GivenAlbumRelease() - { - return _fixture.Build() - .Without(x => x.Album) - .Without(x => x.Tracks) - .Create(); - } - - [Test] - public void two_equivalent_album_releases_should_be_equal() - { - var item1 = GivenAlbumRelease(); - var item2 = item1.JsonClone(); - - item1.Should().NotBeSameAs(item2); - item1.Should().Be(item2); - } - - [Test] - [TestCaseSource(typeof(EqualityPropertySource), "TestCases")] - public void two_different_album_releases_should_not_be_equal(PropertyInfo prop) - { - var item1 = GivenAlbumRelease(); - var item2 = item1.JsonClone(); - var different = GivenAlbumRelease(); - - // make item2 different in the property under consideration - var differentEntry = prop.GetValue(different); - prop.SetValue(item2, differentEntry); - - item1.Should().NotBeSameAs(item2); - item1.Should().NotBe(item2); - } - - [Test] - public void metadata_and_db_fields_should_replicate_release() - { - var item1 = GivenAlbumRelease(); - var item2 = GivenAlbumRelease(); - - item1.Should().NotBe(item2); - - item1.UseMetadataFrom(item2); - item1.UseDbFieldsFrom(item2); - item1.Should().Be(item2); - } - - private Album GivenAlbum() - { - return _fixture.Build() - .Without(x => x.ArtistMetadata) - .Without(x => x.AlbumReleases) - .Without(x => x.Artist) - .Without(x => x.ArtistId) + return _fixture.Build() + .Without(x => x.AuthorMetadata) + .Without(x => x.Author) + .Without(x => x.AuthorId) .Create(); } @@ -208,7 +109,7 @@ namespace NzbDrone.Core.Test.MusicTests } [Test] - [TestCaseSource(typeof(EqualityPropertySource), "TestCases")] + [TestCaseSource(typeof(EqualityPropertySource), "TestCases")] public void two_different_albums_should_not_be_equal(PropertyInfo prop) { var item1 = GivenAlbum(); @@ -242,15 +143,15 @@ namespace NzbDrone.Core.Test.MusicTests item1.Should().Be(item2); } - private Artist GivenArtist() + private Author GivenArtist() { - return _fixture.Build() - .With(x => x.Metadata, new LazyLoaded(_fixture.Create())) + return _fixture.Build() + .With(x => x.Metadata, new LazyLoaded(_fixture.Create())) .Without(x => x.QualityProfile) .Without(x => x.MetadataProfile) - .Without(x => x.Albums) + .Without(x => x.Books) .Without(x => x.Name) - .Without(x => x.ForeignArtistId) + .Without(x => x.ForeignAuthorId) .Create(); } @@ -265,7 +166,7 @@ namespace NzbDrone.Core.Test.MusicTests } [Test] - [TestCaseSource(typeof(EqualityPropertySource), "TestCases")] + [TestCaseSource(typeof(EqualityPropertySource), "TestCases")] public void two_different_artists_should_not_be_equal(PropertyInfo prop) { var item1 = GivenArtist(); diff --git a/src/NzbDrone.Core.Test/MusicTests/MoveArtistServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/MoveArtistServiceFixture.cs index bd448cc16..fdcd0c3c6 100644 --- a/src/NzbDrone.Core.Test/MusicTests/MoveArtistServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/MoveArtistServiceFixture.cs @@ -16,20 +16,20 @@ namespace NzbDrone.Core.Test.MusicTests [TestFixture] public class MoveArtistServiceFixture : CoreTest { - private Artist _artist; + private Author _artist; private MoveArtistCommand _command; private BulkMoveArtistCommand _bulkCommand; [SetUp] public void Setup() { - _artist = Builder + _artist = Builder .CreateNew() .Build(); _command = new MoveArtistCommand { - ArtistId = 1, + AuthorId = 1, SourcePath = @"C:\Test\Music\Artist".AsOsAgnostic(), DestinationPath = @"C:\Test\Music2\Artist".AsOsAgnostic() }; @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.MusicTests { new BulkMoveArtist { - ArtistId = 1, + AuthorId = 1, SourcePath = @"C:\Test\Music\Artist".AsOsAgnostic() } }, @@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.MusicTests ExceptionVerification.ExpectedErrors(1); Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.IsAny()), Times.Once()); + .Verify(v => v.UpdateArtist(It.IsAny()), Times.Once()); } [Test] @@ -100,7 +100,7 @@ namespace NzbDrone.Core.Test.MusicTests Times.Once()); Mocker.GetMock() - .Verify(v => v.GetArtistFolder(It.IsAny(), null), Times.Never()); + .Verify(v => v.GetArtistFolder(It.IsAny(), null), Times.Never()); } [Test] @@ -110,7 +110,7 @@ namespace NzbDrone.Core.Test.MusicTests var expectedPath = Path.Combine(_bulkCommand.DestinationRootFolder, artistFolder); Mocker.GetMock() - .Setup(s => s.GetArtistFolder(It.IsAny(), null)) + .Setup(s => s.GetArtistFolder(It.IsAny(), null)) .Returns(artistFolder); Subject.Execute(_bulkCommand); @@ -141,7 +141,7 @@ namespace NzbDrone.Core.Test.MusicTests It.IsAny()), Times.Never()); Mocker.GetMock() - .Verify(v => v.GetArtistFolder(It.IsAny(), null), Times.Never()); + .Verify(v => v.GetArtistFolder(It.IsAny(), null), Times.Never()); } } } diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs deleted file mode 100644 index f1df30bb2..000000000 --- a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Music; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.MusicTests -{ - [TestFixture] - public class RefreshAlbumReleaseServiceFixture : CoreTest - { - private AlbumRelease _release; - private List _tracks; - private ArtistMetadata _metadata; - - [SetUp] - public void Setup() - { - _release = Builder - .CreateNew() - .With(s => s.Media = new List { new Medium { Number = 1 } }) - .With(s => s.ForeignReleaseId = "xxx-xxx-xxx-xxx") - .With(s => s.Monitored = true) - .With(s => s.TrackCount = 10) - .Build(); - - _metadata = Builder.CreateNew().Build(); - - _tracks = Builder - .CreateListOfSize(10) - .All() - .With(x => x.AlbumReleaseId = _release.Id) - .With(x => x.ArtistMetadata = _metadata) - .With(x => x.ArtistMetadataId = _metadata.Id) - .BuildList(); - - Mocker.GetMock() - .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny>())) - .Returns(_tracks); - } - - [Test] - public void should_update_if_musicbrainz_id_changed_and_no_clash() - { - var newInfo = _release.JsonClone(); - newInfo.ForeignReleaseId = _release.ForeignReleaseId + 1; - newInfo.OldForeignReleaseIds = new List { _release.ForeignReleaseId }; - newInfo.Tracks = _tracks; - - Subject.RefreshEntityInfo(_release, new List { newInfo }, false, false, null); - - Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignReleaseId == newInfo.ForeignReleaseId))); - } - - [Test] - public void should_merge_if_musicbrainz_id_changed_and_new_already_exists() - { - var existing = _release; - - var clash = existing.JsonClone(); - clash.Id = 100; - clash.ForeignReleaseId = clash.ForeignReleaseId + 1; - - clash.Tracks = Builder.CreateListOfSize(10) - .All() - .With(x => x.AlbumReleaseId = clash.Id) - .With(x => x.ArtistMetadata = _metadata) - .With(x => x.ArtistMetadataId = _metadata.Id) - .BuildList(); - - Mocker.GetMock() - .Setup(x => x.GetReleaseByForeignReleaseId(clash.ForeignReleaseId, false)) - .Returns(clash); - - Mocker.GetMock() - .Setup(x => x.GetTracksForRefresh(It.IsAny(), It.IsAny>())) - .Returns(_tracks); - - var newInfo = existing.JsonClone(); - newInfo.ForeignReleaseId = _release.ForeignReleaseId + 1; - newInfo.OldForeignReleaseIds = new List { _release.ForeignReleaseId }; - newInfo.Tracks = _tracks; - - Subject.RefreshEntityInfo(new List { clash, existing }, new List { newInfo }, false, false); - - // check old album is deleted - Mocker.GetMock() - .Verify(v => v.DeleteMany(It.Is>(x => x.First().ForeignReleaseId == existing.ForeignReleaseId))); - - // check that clash gets updated - Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignReleaseId == newInfo.ForeignReleaseId))); - } - - [Test] - public void child_merge_targets_should_not_be_null_if_target_is_new() - { - var oldTrack = Builder - .CreateNew() - .With(x => x.AlbumReleaseId = _release.Id) - .With(x => x.ArtistMetadata = _metadata) - .With(x => x.ArtistMetadataId = _metadata.Id) - .Build(); - _release.Tracks = new List { oldTrack }; - - var newInfo = _release.JsonClone(); - var newTrack = oldTrack.JsonClone(); - newTrack.ArtistMetadata = _metadata; - newTrack.ArtistMetadataId = _metadata.Id; - newTrack.ForeignTrackId = "new id"; - newTrack.OldForeignTrackIds = new List { oldTrack.ForeignTrackId }; - newInfo.Tracks = new List { newTrack }; - - Mocker.GetMock() - .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny>())) - .Returns(new List { oldTrack }); - - Subject.RefreshEntityInfo(_release, new List { newInfo }, false, false, null); - - Mocker.GetMock() - .Verify(v => v.RefreshTrackInfo(It.IsAny>(), - It.IsAny>(), - It.Is>>(x => x.All(y => y.Item2 != null)), - It.IsAny>(), - It.IsAny>(), - It.IsAny>(), - It.IsAny())); - - Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignReleaseId == newInfo.ForeignReleaseId))); - } - } -} diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs index 55393fa4c..cbe2b07f0 100644 --- a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs @@ -18,92 +18,60 @@ namespace NzbDrone.Core.Test.MusicTests [TestFixture] public class RefreshAlbumServiceFixture : CoreTest { - private readonly List _fakeArtists = new List { new ArtistMetadata() }; - private readonly string _fakeArtistForeignId = "xxx-xxx-xxx"; - private Artist _artist; - private List _albums; - private List _releases; + private Author _artist; + private List _albums; [SetUp] public void Setup() { - var release = Builder - .CreateNew() - .With(s => s.Media = new List { new Medium { Number = 1 } }) - .With(s => s.ForeignReleaseId = "xxx-xxx-xxx-xxx") - .With(s => s.Monitored = true) - .With(s => s.TrackCount = 10) - .Build(); - - _releases = new List { release }; - - var album1 = Builder.CreateNew() - .With(x => x.ArtistMetadata = Builder.CreateNew().Build()) + var album1 = Builder.CreateNew() + .With(x => x.AuthorMetadata = Builder.CreateNew().Build()) .With(s => s.Id = 1234) - .With(s => s.ForeignAlbumId = "1") - .With(s => s.AlbumReleases = _releases) + .With(s => s.ForeignBookId = "1") .Build(); - _albums = new List { album1 }; + _albums = new List { album1 }; - _artist = Builder.CreateNew() - .With(s => s.Albums = _albums) + _artist = Builder.CreateNew() + .With(s => s.Books = _albums) .Build(); Mocker.GetMock() .Setup(s => s.GetArtist(_artist.Id)) .Returns(_artist); - Mocker.GetMock() - .Setup(s => s.GetReleasesForRefresh(album1.Id, It.IsAny>())) - .Returns(new List { release }); - Mocker.GetMock() - .Setup(s => s.UpsertMany(It.IsAny>())) + .Setup(s => s.UpsertMany(It.IsAny>())) .Returns(true); - Mocker.GetMock() - .Setup(s => s.GetAlbumInfo(It.IsAny())) - .Callback(() => { throw new AlbumNotFoundException(album1.ForeignAlbumId); }); + Mocker.GetMock() + .Setup(s => s.GetBookInfo(It.IsAny())) + .Callback(() => { throw new AlbumNotFoundException(album1.ForeignBookId); }); Mocker.GetMock() - .Setup(s => s.ShouldRefresh(It.IsAny())) + .Setup(s => s.ShouldRefresh(It.IsAny())) .Returns(true); Mocker.GetMock() .Setup(x => x.GetFilesByAlbum(It.IsAny())) - .Returns(new List()); - - Mocker.GetMock() - .Setup(x => x.GetFilesByRelease(It.IsAny())) - .Returns(new List()); + .Returns(new List()); Mocker.GetMock() .Setup(x => x.GetByAlbum(It.IsAny(), It.IsAny())) .Returns(new List()); } - private void GivenNewAlbumInfo(Album album) - { - Mocker.GetMock() - .Setup(s => s.GetAlbumInfo(_albums.First().ForeignAlbumId)) - .Returns(new Tuple>(_fakeArtistForeignId, album, _fakeArtists)); - } - [Test] public void should_update_if_musicbrainz_id_changed_and_no_clash() { var newAlbumInfo = _albums.First().JsonClone(); - newAlbumInfo.ArtistMetadata = _albums.First().ArtistMetadata.Value.JsonClone(); - newAlbumInfo.ForeignAlbumId = _albums.First().ForeignAlbumId + 1; - newAlbumInfo.AlbumReleases = _releases; + newAlbumInfo.AuthorMetadata = _albums.First().AuthorMetadata.Value.JsonClone(); + newAlbumInfo.ForeignBookId = _albums.First().ForeignBookId + 1; - GivenNewAlbumInfo(newAlbumInfo); - - Subject.RefreshAlbumInfo(_albums, null, false, false, null); + Subject.RefreshAlbumInfo(_albums, new List { newAlbumInfo }, null, false, false, null); Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignAlbumId == newAlbumInfo.ForeignAlbumId))); + .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignBookId == newAlbumInfo.ForeignBookId))); } [Test] @@ -113,286 +81,28 @@ namespace NzbDrone.Core.Test.MusicTests var clash = existing.JsonClone(); clash.Id = 100; - clash.ArtistMetadata = existing.ArtistMetadata.Value.JsonClone(); - clash.ForeignAlbumId = clash.ForeignAlbumId + 1; - - clash.AlbumReleases = Builder.CreateListOfSize(10) - .All().With(x => x.AlbumId = clash.Id) - .BuildList(); + clash.AuthorMetadata = existing.AuthorMetadata.Value.JsonClone(); + clash.ForeignBookId += 1; Mocker.GetMock() - .Setup(x => x.FindById(clash.ForeignAlbumId)) + .Setup(x => x.FindById(clash.ForeignBookId)) .Returns(clash); - Mocker.GetMock() - .Setup(x => x.GetReleasesByAlbum(_albums.First().Id)) - .Returns(_releases); - - Mocker.GetMock() - .Setup(x => x.GetReleasesByAlbum(clash.Id)) - .Returns(new List()); - - Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) - .Returns(_releases); - var newAlbumInfo = existing.JsonClone(); - newAlbumInfo.ArtistMetadata = existing.ArtistMetadata.Value.JsonClone(); - newAlbumInfo.ForeignAlbumId = _albums.First().ForeignAlbumId + 1; - newAlbumInfo.AlbumReleases = _releases; - - GivenNewAlbumInfo(newAlbumInfo); - - Subject.RefreshAlbumInfo(_albums, null, false, false, null); + newAlbumInfo.AuthorMetadata = existing.AuthorMetadata.Value.JsonClone(); + newAlbumInfo.ForeignBookId = _albums.First().ForeignBookId + 1; - // check releases moved to clashing album - Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(x => x.All(y => y.AlbumId == clash.Id) && x.Count == _releases.Count))); + Subject.RefreshAlbumInfo(_albums, new List { newAlbumInfo }, null, false, false, null); // check old album is deleted Mocker.GetMock() - .Verify(v => v.DeleteMany(It.Is>(x => x.First().ForeignAlbumId == existing.ForeignAlbumId))); + .Verify(v => v.DeleteMany(It.Is>(x => x.First().ForeignBookId == existing.ForeignBookId))); // check that clash gets updated Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignAlbumId == newAlbumInfo.ForeignAlbumId))); + .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignBookId == newAlbumInfo.ForeignBookId))); ExceptionVerification.ExpectedWarns(1); } - - [Test] - public void should_remove_album_with_no_valid_releases() - { - var album = _albums.First(); - album.AlbumReleases = new List(); - - GivenNewAlbumInfo(album); - - Subject.RefreshAlbumInfo(album, null, false); - - Mocker.GetMock() - .Verify(x => x.DeleteAlbum(album.Id, true, false), - Times.Once()); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_not_add_duplicate_releases() - { - var newAlbum = Builder.CreateNew() - .With(x => x.ArtistMetadata = Builder.CreateNew().Build()) - .Build(); - - // this is required because RefreshAlbumInfo will edit the album passed in - var albumCopy = Builder.CreateNew() - .With(x => x.ArtistMetadata = Builder.CreateNew().Build()) - .Build(); - - var releases = Builder.CreateListOfSize(10) - .All() - .With(x => x.AlbumId = newAlbum.Id) - .With(x => x.Monitored = true) - .TheFirst(4) - .With(x => x.ForeignReleaseId = "DuplicateId1") - .TheLast(1) - .With(x => x.ForeignReleaseId = "DuplicateId2") - .Build() as List; - - newAlbum.AlbumReleases = releases; - albumCopy.AlbumReleases = releases; - - var existingReleases = Builder.CreateListOfSize(1) - .TheFirst(1) - .With(x => x.ForeignReleaseId = "DuplicateId2") - .With(x => x.Monitored = true) - .Build() as List; - - Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) - .Returns(existingReleases); - - Mocker.GetMock() - .Setup(x => x.GetAlbumInfo(It.IsAny())) - .Returns(Tuple.Create("dummy string", albumCopy, new List())); - - Subject.RefreshAlbumInfo(newAlbum, null, false); - - Mocker.GetMock() - .Verify(x => x.RefreshEntityInfo(It.Is>(l => l.Count == 7 && l.Count(y => y.Monitored) == 1), - It.IsAny>(), - It.IsAny(), - It.IsAny())); - } - - [TestCase(true, true, 1)] - [TestCase(true, false, 0)] - [TestCase(false, true, 1)] - [TestCase(false, false, 0)] - public void should_only_leave_one_release_monitored(bool skyhookMonitored, bool existingMonitored, int expectedUpdates) - { - var newAlbum = Builder.CreateNew() - .With(x => x.ArtistMetadata = Builder.CreateNew().Build()) - .Build(); - - // this is required because RefreshAlbumInfo will edit the album passed in - var albumCopy = Builder.CreateNew() - .With(x => x.ArtistMetadata = Builder.CreateNew().Build()) - .Build(); - - var releases = Builder.CreateListOfSize(10) - .All() - .With(x => x.AlbumId = newAlbum.Id) - .With(x => x.Monitored = skyhookMonitored) - .TheFirst(1) - .With(x => x.ForeignReleaseId = "ExistingId1") - .TheNext(1) - .With(x => x.ForeignReleaseId = "ExistingId2") - .Build() as List; - - newAlbum.AlbumReleases = releases; - albumCopy.AlbumReleases = releases; - - var existingReleases = Builder.CreateListOfSize(2) - .All() - .With(x => x.Monitored = existingMonitored) - .TheFirst(1) - .With(x => x.ForeignReleaseId = "ExistingId1") - .TheNext(1) - .With(x => x.ForeignReleaseId = "ExistingId2") - .Build() as List; - - Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) - .Returns(existingReleases); - - Mocker.GetMock() - .Setup(x => x.GetAlbumInfo(It.IsAny())) - .Returns(Tuple.Create("dummy string", albumCopy, new List())); - - Subject.RefreshAlbumInfo(newAlbum, null, false); - - Mocker.GetMock() - .Verify(x => x.RefreshEntityInfo(It.Is>(l => l.Count == 10 && l.Count(y => y.Monitored) == 1), - It.IsAny>(), - It.IsAny(), - It.IsAny())); - } - - [Test] - public void refreshing_album_should_not_change_monitored_release_if_monitored_release_not_deleted() - { - var newAlbum = Builder.CreateNew() - .With(x => x.ArtistMetadata = Builder.CreateNew().Build()) - .Build(); - - // this is required because RefreshAlbumInfo will edit the album passed in - var albumCopy = Builder.CreateNew() - .With(x => x.ArtistMetadata = Builder.CreateNew().Build()) - .Build(); - - // only ExistingId1 is monitored from dummy skyhook - var releases = Builder.CreateListOfSize(10) - .All() - .With(x => x.AlbumId = newAlbum.Id) - .With(x => x.Monitored = false) - .TheFirst(1) - .With(x => x.ForeignReleaseId = "ExistingId1") - .With(x => x.Monitored = true) - .TheNext(1) - .With(x => x.ForeignReleaseId = "ExistingId2") - .Build() as List; - - newAlbum.AlbumReleases = releases; - albumCopy.AlbumReleases = releases; - - // ExistingId2 is monitored in DB - var existingReleases = Builder.CreateListOfSize(2) - .All() - .With(x => x.Monitored = false) - .TheFirst(1) - .With(x => x.ForeignReleaseId = "ExistingId1") - .TheNext(1) - .With(x => x.ForeignReleaseId = "ExistingId2") - .With(x => x.Monitored = true) - .Build() as List; - - Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) - .Returns(existingReleases); - - Mocker.GetMock() - .Setup(x => x.GetAlbumInfo(It.IsAny())) - .Returns(Tuple.Create("dummy string", albumCopy, new List())); - - Subject.RefreshAlbumInfo(newAlbum, null, false); - - Mocker.GetMock() - .Verify(x => x.RefreshEntityInfo(It.Is>( - l => l.Count == 10 && - l.Count(y => y.Monitored) == 1 && - l.Single(y => y.Monitored).ForeignReleaseId == "ExistingId2"), - It.IsAny>(), - It.IsAny(), - It.IsAny())); - } - - [Test] - public void refreshing_album_should_change_monitored_release_if_monitored_release_deleted() - { - var newAlbum = Builder.CreateNew() - .With(x => x.ArtistMetadata = Builder.CreateNew().Build()) - .Build(); - - // this is required because RefreshAlbumInfo will edit the album passed in - var albumCopy = Builder.CreateNew() - .With(x => x.ArtistMetadata = Builder.CreateNew().Build()) - .Build(); - - // Only existingId1 monitored in skyhook. ExistingId2 is missing - var releases = Builder.CreateListOfSize(10) - .All() - .With(x => x.AlbumId = newAlbum.Id) - .With(x => x.Monitored = false) - .TheFirst(1) - .With(x => x.ForeignReleaseId = "ExistingId1") - .With(x => x.Monitored = true) - .TheNext(1) - .With(x => x.ForeignReleaseId = "NotExistingId2") - .Build() as List; - - newAlbum.AlbumReleases = releases; - albumCopy.AlbumReleases = releases; - - // ExistingId2 is monitored but will be deleted - var existingReleases = Builder.CreateListOfSize(2) - .All() - .With(x => x.Monitored = false) - .TheFirst(1) - .With(x => x.ForeignReleaseId = "ExistingId1") - .TheNext(1) - .With(x => x.ForeignReleaseId = "ExistingId2") - .With(x => x.Monitored = true) - .Build() as List; - - Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) - .Returns(existingReleases); - - Mocker.GetMock() - .Setup(x => x.GetAlbumInfo(It.IsAny())) - .Returns(Tuple.Create("dummy string", albumCopy, new List())); - - Subject.RefreshAlbumInfo(newAlbum, null, false); - - Mocker.GetMock() - .Verify(x => x.RefreshEntityInfo(It.Is>( - l => l.Count == 11 && - l.Count(y => y.Monitored) == 1 && - l.Single(y => y.Monitored).ForeignReleaseId != "ExistingId2"), - It.IsAny>(), - It.IsAny(), - It.IsAny())); - } } } diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs index c3f384ae6..f08d89726 100644 --- a/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Music; using NzbDrone.Core.Music.Commands; using NzbDrone.Core.Music.Events; +using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; @@ -20,48 +21,54 @@ namespace NzbDrone.Core.Test.MusicTests [TestFixture] public class RefreshArtistServiceFixture : CoreTest { - private Artist _artist; - private Album _album1; - private Album _album2; - private List _albums; - private List _remoteAlbums; + private Author _artist; + private Book _album1; + private Book _album2; + private List _albums; + private List _remoteAlbums; [SetUp] public void Setup() { - _album1 = Builder.CreateNew() - .With(s => s.ForeignAlbumId = "1") + _album1 = Builder.CreateNew() + .With(s => s.ForeignBookId = "1") .Build(); - _album2 = Builder.CreateNew() - .With(s => s.ForeignAlbumId = "2") + _album2 = Builder.CreateNew() + .With(s => s.ForeignBookId = "2") .Build(); - _albums = new List { _album1, _album2 }; + _albums = new List { _album1, _album2 }; _remoteAlbums = _albums.JsonClone(); _remoteAlbums.ForEach(x => x.Id = 0); - var metadata = Builder.CreateNew().Build(); + var metadata = Builder.CreateNew().Build(); + var series = Builder.CreateListOfSize(1).BuildList(); - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(a => a.Metadata = metadata) + .With(a => a.Series = series) .Build(); Mocker.GetMock(MockBehavior.Strict) .Setup(s => s.GetArtists(new List { _artist.Id })) - .Returns(new List { _artist }); + .Returns(new List { _artist }); Mocker.GetMock(MockBehavior.Strict) - .Setup(s => s.InsertMany(It.IsAny>())); + .Setup(s => s.InsertMany(It.IsAny>())); - Mocker.GetMock() - .Setup(s => s.GetArtistInfo(It.IsAny(), It.IsAny())) - .Callback(() => { throw new ArtistNotFoundException(_artist.ForeignArtistId); }); + Mocker.GetMock() + .Setup(s => s.FilterBooks(It.IsAny(), It.IsAny())) + .Returns(_albums); + + Mocker.GetMock() + .Setup(s => s.GetAuthorInfo(It.IsAny())) + .Callback(() => { throw new ArtistNotFoundException(_artist.ForeignAuthorId); }); Mocker.GetMock() .Setup(x => x.GetFilesByArtist(It.IsAny())) - .Returns(new List()); + .Returns(new List()); Mocker.GetMock() .Setup(x => x.GetByArtist(It.IsAny(), It.IsAny())) @@ -76,10 +83,10 @@ namespace NzbDrone.Core.Test.MusicTests .Returns(new List()); } - private void GivenNewArtistInfo(Artist artist) + private void GivenNewArtistInfo(Author artist) { - Mocker.GetMock() - .Setup(s => s.GetArtistInfo(_artist.ForeignArtistId, _artist.MetadataProfileId)) + Mocker.GetMock() + .Setup(s => s.GetAuthorInfo(_artist.ForeignAuthorId)) .Returns(artist); } @@ -87,10 +94,10 @@ namespace NzbDrone.Core.Test.MusicTests { Mocker.GetMock() .Setup(x => x.GetFilesByArtist(It.IsAny())) - .Returns(Builder.CreateListOfSize(1).BuildList()); + .Returns(Builder.CreateListOfSize(1).BuildList()); } - private void GivenAlbumsForRefresh(List albums) + private void GivenAlbumsForRefresh(List albums) { Mocker.GetMock(MockBehavior.Strict) .Setup(s => s.GetAlbumsForRefresh(It.IsAny(), It.IsAny>())) @@ -100,8 +107,8 @@ namespace NzbDrone.Core.Test.MusicTests private void AllowArtistUpdate() { Mocker.GetMock(MockBehavior.Strict) - .Setup(x => x.UpdateArtist(It.IsAny())) - .Returns((Artist a) => a); + .Setup(x => x.UpdateArtist(It.IsAny())) + .Returns((Author a) => a); } [Test] @@ -109,7 +116,7 @@ namespace NzbDrone.Core.Test.MusicTests { var newArtistInfo = _artist.JsonClone(); newArtistInfo.Metadata = _artist.Metadata.Value.JsonClone(); - newArtistInfo.Albums = _remoteAlbums; + newArtistInfo.Books = _remoteAlbums; GivenNewArtistInfo(newArtistInfo); GivenAlbumsForRefresh(_albums); @@ -130,10 +137,10 @@ namespace NzbDrone.Core.Test.MusicTests { new MediaCover.MediaCover(MediaCover.MediaCoverTypes.Logo, "dummy") }; - newArtistInfo.Albums = _remoteAlbums; + newArtistInfo.Books = _remoteAlbums; GivenNewArtistInfo(newArtistInfo); - GivenAlbumsForRefresh(new List()); + GivenAlbumsForRefresh(new List()); AllowArtistUpdate(); Subject.Execute(new RefreshArtistCommand(_artist.Id)); @@ -151,7 +158,7 @@ namespace NzbDrone.Core.Test.MusicTests Subject.Execute(new RefreshArtistCommand(_artist.Id)); Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.IsAny()), Times.Never()); + .Verify(v => v.UpdateArtist(It.IsAny()), Times.Never()); Mocker.GetMock() .Verify(v => v.DeleteArtist(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -164,12 +171,12 @@ namespace NzbDrone.Core.Test.MusicTests public void should_log_error_but_not_delete_if_musicbrainz_id_not_found_and_artist_has_files() { GivenArtistFiles(); - GivenAlbumsForRefresh(new List()); + GivenAlbumsForRefresh(new List()); Subject.Execute(new RefreshArtistCommand(_artist.Id)); Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.IsAny()), Times.Never()); + .Verify(v => v.UpdateArtist(It.IsAny()), Times.Never()); Mocker.GetMock() .Verify(v => v.DeleteArtist(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); @@ -182,8 +189,8 @@ namespace NzbDrone.Core.Test.MusicTests { var newArtistInfo = _artist.JsonClone(); newArtistInfo.Metadata = _artist.Metadata.Value.JsonClone(); - newArtistInfo.Albums = _remoteAlbums; - newArtistInfo.ForeignArtistId = _artist.ForeignArtistId + 1; + newArtistInfo.Books = _remoteAlbums; + newArtistInfo.ForeignAuthorId = _artist.ForeignAuthorId + 1; newArtistInfo.Metadata.Value.Id = 100; GivenNewArtistInfo(newArtistInfo); @@ -191,30 +198,30 @@ namespace NzbDrone.Core.Test.MusicTests var seq = new MockSequence(); Mocker.GetMock(MockBehavior.Strict) - .Setup(x => x.FindById(newArtistInfo.ForeignArtistId)) - .Returns(default(Artist)); + .Setup(x => x.FindById(newArtistInfo.ForeignAuthorId)) + .Returns(default(Author)); // Make sure that the artist is updated before we refresh the albums Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) - .Setup(x => x.UpdateArtist(It.IsAny())) - .Returns((Artist a) => a); + .Setup(x => x.UpdateArtist(It.IsAny())) + .Returns((Author a) => a); Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) .Setup(x => x.GetAlbumsForRefresh(It.IsAny(), It.IsAny>())) - .Returns(new List()); + .Returns(new List()); // Update called twice for a move/merge Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) - .Setup(x => x.UpdateArtist(It.IsAny())) - .Returns((Artist a) => a); + .Setup(x => x.UpdateArtist(It.IsAny())) + .Returns((Author a) => a); Subject.Execute(new RefreshArtistCommand(_artist.Id)); Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.Is(s => s.ArtistMetadataId == 100 && s.ForeignArtistId == newArtistInfo.ForeignArtistId)), + .Verify(v => v.UpdateArtist(It.Is(s => s.AuthorMetadataId == 100 && s.ForeignAuthorId == newArtistInfo.ForeignAuthorId)), Times.Exactly(2)); } @@ -227,15 +234,15 @@ namespace NzbDrone.Core.Test.MusicTests clash.Id = 100; clash.Metadata = existing.Metadata.Value.JsonClone(); clash.Metadata.Value.Id = 101; - clash.Metadata.Value.ForeignArtistId = clash.Metadata.Value.ForeignArtistId + 1; + clash.Metadata.Value.ForeignAuthorId = clash.Metadata.Value.ForeignAuthorId + 1; Mocker.GetMock(MockBehavior.Strict) - .Setup(x => x.FindById(clash.Metadata.Value.ForeignArtistId)) + .Setup(x => x.FindById(clash.Metadata.Value.ForeignAuthorId)) .Returns(clash); var newArtistInfo = clash.JsonClone(); newArtistInfo.Metadata = clash.Metadata.Value.JsonClone(); - newArtistInfo.Albums = _remoteAlbums; + newArtistInfo.Books = _remoteAlbums; GivenNewArtistInfo(newArtistInfo); @@ -249,7 +256,7 @@ namespace NzbDrone.Core.Test.MusicTests Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) - .Setup(x => x.UpdateMany(It.IsAny>())); + .Setup(x => x.UpdateMany(It.IsAny>())); Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) @@ -257,32 +264,32 @@ namespace NzbDrone.Core.Test.MusicTests Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) - .Setup(x => x.UpdateArtist(It.Is(a => a.Id == clash.Id))) - .Returns((Artist a) => a); + .Setup(x => x.UpdateArtist(It.Is(a => a.Id == clash.Id))) + .Returns((Author a) => a); Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) - .Setup(x => x.GetAlbumsForRefresh(clash.ArtistMetadataId, It.IsAny>())) + .Setup(x => x.GetAlbumsForRefresh(clash.AuthorMetadataId, It.IsAny>())) .Returns(_albums); // Update called twice for a move/merge Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) - .Setup(x => x.UpdateArtist(It.IsAny())) - .Returns((Artist a) => a); + .Setup(x => x.UpdateArtist(It.IsAny())) + .Returns((Author a) => a); Subject.Execute(new RefreshArtistCommand(_artist.Id)); // the retained artist gets updated Mocker.GetMock() - .Verify(v => v.UpdateArtist(It.Is(s => s.Id == clash.Id)), Times.Exactly(2)); + .Verify(v => v.UpdateArtist(It.Is(s => s.Id == clash.Id)), Times.Exactly(2)); // the old one gets removed Mocker.GetMock() .Verify(v => v.DeleteArtist(existing.Id, false, false)); Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(x => x.Count == _albums.Count))); + .Verify(v => v.UpdateMany(It.Is>(x => x.Count == _albums.Count))); ExceptionVerification.ExpectedWarns(1); } diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshTrackServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshTrackServiceFixture.cs deleted file mode 100644 index ceb928a35..000000000 --- a/src/NzbDrone.Core.Test/MusicTests/RefreshTrackServiceFixture.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.MusicTests -{ - [TestFixture] - public class RefreshTrackServiceFixture : CoreTest - { - private AlbumRelease _release; - private List _allTracks; - - [SetUp] - public void Setup() - { - _release = Builder.CreateNew().Build(); - _allTracks = Builder.CreateListOfSize(20) - .All() - .BuildList(); - } - - [Test] - public void updated_track_should_not_have_null_album_release() - { - var add = new List(); - var update = new List(); - var merge = new List>(); - var delete = new List(); - var upToDate = new List(); - - upToDate.AddRange(_allTracks.Take(10)); - - var toUpdate = _allTracks[10].JsonClone(); - toUpdate.Title = "title to update"; - toUpdate.AlbumRelease = _release; - - update.Add(toUpdate); - - Subject.RefreshTrackInfo(add, update, merge, delete, upToDate, _allTracks, false); - - Mocker.GetMock() - .Verify(v => v.SyncTags(It.Is>(x => x.Count == 1 && - x[0].AlbumRelease != null && - x[0].AlbumRelease.IsLoaded == true))); - } - } -} diff --git a/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshAlbumFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshAlbumFixture.cs index 99361dde8..dde9dd1f6 100644 --- a/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshAlbumFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshAlbumFixture.cs @@ -10,12 +10,12 @@ namespace NzbDrone.Core.Test.MusicTests [TestFixture] public class ShouldRefreshAlbumFixture : TestBase { - private Album _album; + private Book _album; [SetUp] public void Setup() { - _album = Builder.CreateNew() + _album = Builder.CreateNew() .With(e => e.ReleaseDate = DateTime.Today.AddDays(-100)) .Build(); } diff --git a/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshArtistFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshArtistFixture.cs index 33ca92cc7..d83b967b8 100644 --- a/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshArtistFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/ShouldRefreshArtistFixture.cs @@ -11,18 +11,18 @@ namespace NzbDrone.Core.Test.MusicTests [TestFixture] public class ShouldRefreshArtistFixture : TestBase { - private Artist _artist; + private Author _artist; [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(v => v.Metadata.Value.Status == ArtistStatusType.Continuing) .Build(); Mocker.GetMock() .Setup(s => s.GetAlbumsByArtist(_artist.Id)) - .Returns(Builder.CreateListOfSize(2) + .Returns(Builder.CreateListOfSize(2) .All() .With(e => e.ReleaseDate = DateTime.Today.AddDays(-100)) .Build() @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Test.MusicTests { Mocker.GetMock() .Setup(s => s.GetAlbumsByArtist(_artist.Id)) - .Returns(Builder.CreateListOfSize(2) + .Returns(Builder.CreateListOfSize(2) .TheFirst(1) .With(e => e.ReleaseDate = DateTime.Today.AddDays(-7)) .TheLast(1) diff --git a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs index 0dadac30f..7a207c1cc 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Test.NotificationTests TestLogger.Info("OnAlbumDownload was called"); } - public override void OnRename(Artist artist) + public override void OnRename(Author artist) { TestLogger.Info("OnRename was called"); } diff --git a/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs index a241199e1..3db6af3c2 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs @@ -14,14 +14,14 @@ namespace NzbDrone.Core.Test.NotificationTests [TestFixture] public class SynologyIndexerFixture : CoreTest { - private Artist _artist; + private Author _artist; private AlbumDownloadMessage _upgrade; private string _rootPath = @"C:\Test\".AsOsAgnostic(); [SetUp] public void SetUp() { - _artist = new Artist() + _artist = new Author() { Path = _rootPath, }; @@ -30,21 +30,21 @@ namespace NzbDrone.Core.Test.NotificationTests { Artist = _artist, - TrackFiles = new List + TrackFiles = new List { - new TrackFile + new BookFile { Path = Path.Combine(_rootPath, "file1.S01E01E02.mkv") } }, - OldFiles = new List + OldFiles = new List { - new TrackFile + new BookFile { Path = Path.Combine(_rootPath, "file1.S01E01.mkv") }, - new TrackFile + new BookFile { Path = Path.Combine(_rootPath, "file1.S01E02.mkv") } diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/GetArtistPathFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/GetArtistPathFixture.cs deleted file mode 100644 index 4340555e5..000000000 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/GetArtistPathFixture.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Music; -using NzbDrone.Core.Notifications.Xbmc; -using NzbDrone.Core.Notifications.Xbmc.Model; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.NotificationTests.Xbmc -{ - [TestFixture] - public class GetArtistPathFixture : CoreTest - { - private const string MB_ID = "9f4e41c3-2648-428e-b8c7-dc10465b49ac"; - private XbmcSettings _settings; - private Music.Artist _artist; - private List _xbmcArtist; - - [SetUp] - public void Setup() - { - _settings = Builder.CreateNew() - .Build(); - - _xbmcArtist = Builder.CreateListOfSize(3) - .All() - .With(s => s.MusicbrainzArtistId = new List { "0" }) - .TheFirst(1) - .With(s => s.MusicbrainzArtistId = new List { MB_ID.ToString() }) - .Build() - .ToList(); - - Mocker.GetMock() - .Setup(s => s.GetArtist(_settings)) - .Returns(_xbmcArtist); - } - - private void GivenMatchingMusicbrainzId() - { - _artist = new Artist - { - ForeignArtistId = MB_ID, - Name = "Artist" - }; - } - - private void GivenMatchingTitle() - { - _artist = new Artist - { - ForeignArtistId = "1000", - Name = _xbmcArtist.First().Label - }; - } - - private void GivenMatchingArtist() - { - _artist = new Artist - { - ForeignArtistId = "1000", - Name = "Does not exist" - }; - } - - [Test] - public void should_return_null_when_artist_is_not_found() - { - GivenMatchingArtist(); - - Subject.GetArtistPath(_settings, _artist).Should().BeNull(); - } - - [Test] - public void should_return_path_when_musicbrainzId_matches() - { - GivenMatchingMusicbrainzId(); - - Subject.GetArtistPath(_settings, _artist).Should().Be(_xbmcArtist.First().File); - } - - [Test] - public void should_return_path_when_title_matches() - { - GivenMatchingTitle(); - - Subject.GetArtistPath(_settings, _artist).Should().Be(_xbmcArtist.First().File); - } - } -} diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnReleaseImportFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnReleaseImportFixture.cs deleted file mode 100644 index 6fed550f5..000000000 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/OnReleaseImportFixture.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; -using NzbDrone.Core.Notifications; -using NzbDrone.Core.Notifications.Xbmc; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.NotificationTests.Xbmc -{ - [TestFixture] - public class OnReleaseImportFixture : CoreTest - { - private AlbumDownloadMessage _albumDownloadMessage; - - [SetUp] - public void Setup() - { - var artist = Builder.CreateNew() - .Build(); - - var trackFile = Builder.CreateNew() - .Build(); - - _albumDownloadMessage = Builder.CreateNew() - .With(d => d.Artist = artist) - .With(d => d.TrackFiles = new List { trackFile }) - .With(d => d.OldFiles = new List()) - .Build(); - - Subject.Definition = new NotificationDefinition(); - Subject.Definition.Settings = new XbmcSettings - { - UpdateLibrary = true - }; - } - - private void GivenOldFiles() - { - _albumDownloadMessage.OldFiles = Builder.CreateListOfSize(1) - .Build() - .ToList(); - - Subject.Definition.Settings = new XbmcSettings - { - UpdateLibrary = true, - CleanLibrary = true - }; - } - - [Test] - public void should_not_clean_if_no_episode_was_replaced() - { - Subject.OnReleaseImport(_albumDownloadMessage); - - Mocker.GetMock().Verify(v => v.Clean(It.IsAny()), Times.Never()); - } - - [Test] - public void should_clean_if_episode_was_replaced() - { - GivenOldFiles(); - Subject.OnReleaseImport(_albumDownloadMessage); - - Mocker.GetMock().Verify(v => v.Clean(It.IsAny()), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/UpdateFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/UpdateFixture.cs deleted file mode 100644 index 852a7173a..000000000 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/UpdateFixture.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Music; -using NzbDrone.Core.Notifications.Xbmc; -using NzbDrone.Core.Notifications.Xbmc.Model; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.NotificationTests.Xbmc -{ - [TestFixture] - public class UpdateFixture : CoreTest - { - private const string MB_ID = "9f4e41c3-2648-428e-b8c7-dc10465b49ac"; - private XbmcSettings _settings; - private List _xbmcArtist; - - [SetUp] - public void Setup() - { - _settings = Builder.CreateNew() - .Build(); - - _xbmcArtist = Builder.CreateListOfSize(3) - .TheFirst(1) - .With(s => s.MusicbrainzArtistId = new List { MB_ID.ToString() }) - .TheNext(2) - .With(s => s.MusicbrainzArtistId = new List()) - .Build() - .ToList(); - - Mocker.GetMock() - .Setup(s => s.GetArtist(_settings)) - .Returns(_xbmcArtist); - - Mocker.GetMock() - .Setup(s => s.GetActivePlayers(_settings)) - .Returns(new List()); - } - - [Test] - public void should_update_using_artist_path() - { - var artist = Builder.CreateNew() - .With(s => s.ForeignArtistId = MB_ID) - .Build(); - - Subject.Update(_settings, artist); - - Mocker.GetMock() - .Verify(v => v.UpdateLibrary(_settings, It.IsAny()), Times.Once()); - } - - [Test] - public void should_update_all_paths_when_artist_path_not_found() - { - var fakeArtist = Builder.CreateNew() - .With(s => s.ForeignArtistId = "9f4e41c3-2648-428e-b8c7-dc10465b49ad") - .With(s => s.Name = "Not Shawn Desman") - .Build(); - - Subject.Update(_settings, fakeArtist); - - Mocker.GetMock() - .Verify(v => v.UpdateLibrary(_settings, null), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs index f76c1c262..ee9ef9cc0 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs @@ -9,7 +9,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.OrganizerTests { [TestFixture] - + [Ignore("Don't use album folder in readarr")] public class BuildFilePathFixture : CoreTest { private NamingConfig _namingConfig; @@ -26,22 +26,19 @@ namespace NzbDrone.Core.Test.OrganizerTests [Test] public void should_clean_album_folder_when_it_contains_illegal_characters_in_album_or_artist_title() { - var filename = @"02 - Track Title"; - var expectedPath = @"C:\Test\Fake- The Artist\Fake- The Artist Fake- Album\02 - Track Title.flac"; + var filename = @"bookfile"; + var expectedPath = @"C:\Test\Fake- The Author\Fake- The Book\bookfile.mobi"; - var fakeArtist = Builder.CreateNew() - .With(s => s.Name = "Fake: The Artist") - .With(s => s.Path = @"C:\Test\Fake- The Artist".AsOsAgnostic()) - .With(s => s.AlbumFolder = true) + var fakeArtist = Builder.CreateNew() + .With(s => s.Name = "Fake: The Author") + .With(s => s.Path = @"C:\Test\Fake- The Author".AsOsAgnostic()) .Build(); - var fakeAlbum = Builder.CreateNew() - .With(s => s.Title = "Fake: Album") + var fakeAlbum = Builder.CreateNew() + .With(s => s.Title = "Fake: Book") .Build(); - _namingConfig.AlbumFolderFormat = "{Artist Name} {Album Title}"; - - Subject.BuildTrackFilePath(fakeArtist, fakeAlbum, filename, ".flac").Should().Be(expectedPath.AsOsAgnostic()); + Subject.BuildTrackFilePath(fakeArtist, fakeAlbum, filename, ".mobi").Should().Be(expectedPath.AsOsAgnostic()); } } } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs index de957c7e7..d64b23164 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleFixture.cs @@ -14,38 +14,25 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests [TestFixture] public class CleanTitleFixture : CoreTest { - private Artist _artist; - private Album _album; - private AlbumRelease _release; - private Track _track; - private TrackFile _trackFile; + private Author _artist; + private Book _album; + private BookFile _trackFile; private NamingConfig _namingConfig; [SetUp] public void Setup() { - _artist = Builder + _artist = Builder .CreateNew() .With(s => s.Name = "Avenged Sevenfold") .Build(); - _album = Builder + _album = Builder .CreateNew() .With(s => s.Title = "Hail to the King") .Build(); - _release = Builder - .CreateNew() - .With(s => s.Media = new List { new Medium { Number = 1 } }) - .Build(); - - _track = Builder.CreateNew() - .With(e => e.Title = "Doing Time") - .With(e => e.AbsoluteTrackNumber = 3) - .With(e => e.AlbumRelease = _release) - .Build(); - - _trackFile = new TrackFile { Quality = new QualityModel(Quality.MP3_256), ReleaseGroup = "ReadarrTest" }; + _trackFile = new BookFile { Quality = new QualityModel(Quality.MP3_320), ReleaseGroup = "ReadarrTest" }; _namingConfig = NamingConfig.Default; _namingConfig.RenameTracks = true; @@ -81,27 +68,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _artist.Name = name; _namingConfig.StandardTrackFormat = "{Artist CleanName}"; - Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(expected); } - - [Test] - public void should_use_and_as_separator_for_multiple_episodes() - { - var tracks = Builder.CreateListOfSize(2) - .TheFirst(1) - .With(e => e.Title = "Surrender Benson") - .TheNext(1) - .With(e => e.Title = "Imprisoned Lives") - .All() - .With(e => e.AlbumRelease = _release) - .Build() - .ToList(); - - _namingConfig.StandardTrackFormat = "{Track CleanTitle}"; - - Subject.BuildTrackFileName(tracks, _artist, _album, _trackFile) - .Should().Be(tracks.First().Title + " and " + tracks.Last().Title); - } } } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index c77159594..cb3b6433a 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -16,42 +16,27 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests public class FileNameBuilderFixture : CoreTest { - private Artist _artist; - private Album _album; - private Medium _medium; - private AlbumRelease _release; - private Track _track1; - private TrackFile _trackFile; + private Author _artist; + private Book _album; + private BookFile _trackFile; private NamingConfig _namingConfig; [SetUp] public void Setup() { - _artist = Builder + _artist = Builder .CreateNew() .With(s => s.Name = "Linkin Park") - .With(s => s.Metadata = new ArtistMetadata + .With(s => s.Metadata = new AuthorMetadata { Disambiguation = "US Rock Band", Name = "Linkin Park" }) .Build(); - _medium = Builder - .CreateNew() - .With(m => m.Number = 3) - .Build(); - - _release = Builder - .CreateNew() - .With(s => s.Media = new List { _medium }) - .With(s => s.Monitored = true) - .Build(); - - _album = Builder + _album = Builder .CreateNew() .With(s => s.Title = "Hybrid Theory") - .With(s => s.AlbumType = "Album") .With(s => s.Disambiguation = "The Best Album") .Build(); @@ -61,15 +46,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests Mocker.GetMock() .Setup(c => c.GetConfig()).Returns(_namingConfig); - _track1 = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.AbsoluteTrackNumber = 6) - .With(e => e.AlbumRelease = _release) - .With(e => e.MediumNumber = _medium.Number) - .Build(); - - _trackFile = Builder.CreateNew() - .With(e => e.Quality = new QualityModel(Quality.MP3_256)) + _trackFile = Builder.CreateNew() + .With(e => e.Quality = new QualityModel(Quality.MP3_320)) .With(e => e.ReleaseGroup = "ReadarrTest") .With(e => e.MediaInfo = new Parser.Model.MediaInfoModel { @@ -100,7 +78,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Artist Name}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Linkin Park"); } @@ -109,7 +87,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Artist_Name}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Linkin_Park"); } @@ -118,7 +96,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Artist.Name}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Linkin.Park"); } @@ -127,7 +105,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Artist-Name}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Linkin-Park"); } @@ -136,7 +114,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{ARTIST NAME}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("LINKIN PARK"); } @@ -145,7 +123,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{aRtIST-nAmE}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(_artist.Name.Replace(' ', '-')); } @@ -154,7 +132,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{artist name}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("linkin park"); } @@ -164,7 +142,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _namingConfig.StandardTrackFormat = "{Artist.CleanName}"; _artist.Name = "Linkin Park (1997)"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Linkin.Park.1997"); } @@ -173,7 +151,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Artist Disambiguation}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("US Rock Band"); } @@ -182,25 +160,16 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Album Title}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Hybrid Theory"); } - [Test] - public void should_replace_Album_Type() - { - _namingConfig.StandardTrackFormat = "{Album Type}"; - - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("Album"); - } - [Test] public void should_replace_Album_Disambiguation() { _namingConfig.StandardTrackFormat = "{Album Disambiguation}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("The Best Album"); } @@ -209,7 +178,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Album_Title}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Hybrid_Theory"); } @@ -218,7 +187,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Album.Title}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Hybrid.Theory"); } @@ -227,7 +196,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Album-Title}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Hybrid-Theory"); } @@ -236,7 +205,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{ALBUM TITLE}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("HYBRID THEORY"); } @@ -245,7 +214,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{aLbUM-tItLE}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(_album.Title.Replace(' ', '-')); } @@ -254,7 +223,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{album title}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("hybrid theory"); } @@ -264,73 +233,17 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _namingConfig.StandardTrackFormat = "{Artist.CleanName}"; _artist.Name = "Hybrid Theory (2000)"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Hybrid.Theory.2000"); } - [Test] - public void should_replace_track_title() - { - _namingConfig.StandardTrackFormat = "{Track Title}"; - - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("City Sushi"); - } - - [Test] - public void should_replace_track_title_if_pattern_has_random_casing() - { - _namingConfig.StandardTrackFormat = "{tRaCK-TitLe}"; - - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("City-Sushi"); - } - - [Test] - public void should_replace_track_number_with_single_digit() - { - _track1.AbsoluteTrackNumber = 1; - _namingConfig.StandardTrackFormat = "{track}"; - - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("1"); - } - - [Test] - public void should_replace_track00_number_with_two_digits() - { - _track1.AbsoluteTrackNumber = 1; - _namingConfig.StandardTrackFormat = "{track:00}"; - - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("01"); - } - - [Test] - public void should_replace_medium_number_with_single_digit() - { - _namingConfig.StandardTrackFormat = "{medium}"; - - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("3"); - } - - [Test] - public void should_replace_medium00_number_with_two_digits() - { - _namingConfig.StandardTrackFormat = "{medium:00}"; - - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("03"); - } - [Test] public void should_replace_quality_title() { _namingConfig.StandardTrackFormat = "{Quality Title}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("MP3-256"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("MP3-320"); } [Test] @@ -338,7 +251,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{MediaInfo AudioCodec}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("FLAC"); } @@ -347,7 +260,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{MediaInfo AudioBitRate}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("320 kbps"); } @@ -356,7 +269,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{MediaInfo AudioChannels}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("2.0"); } @@ -365,7 +278,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{MediaInfo AudioBitsPerSample}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("16bit"); } @@ -374,17 +287,17 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{MediaInfo AudioSampleRate}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("44.1kHz"); } [Test] public void should_replace_all_contents_in_pattern() { - _namingConfig.StandardTrackFormat = "{Artist Name} - {Album Title} - {track:00} - {Track Title} [{Quality Title}]"; + _namingConfig.StandardTrackFormat = "{Artist Name} - {Album Title} - [{Quality Title}]"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("Linkin Park - Hybrid Theory - 06 - City Sushi [MP3-256]"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("Linkin Park - Hybrid Theory - [MP3-320]"); } [Test] @@ -393,7 +306,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _namingConfig.RenameTracks = false; _trackFile.Path = "Linkin Park - 06 - Test"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path)); } @@ -404,7 +317,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _trackFile.Path = "Linkin Park - 06 - Test"; _trackFile.SceneName = "SceneName"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path)); } @@ -414,28 +327,16 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _namingConfig.RenameTracks = false; _trackFile.Path = @"C:\Test\Unsorted\Artist - 01 - Test"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path)); } - [Test] - public void should_not_clean_track_title_if_there_is_only_one() - { - var title = "City Sushi (1)"; - _track1.Title = title; - - _namingConfig.StandardTrackFormat = "{Track Title}"; - - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be(title); - } - [Test] public void should_should_replace_release_group() { _namingConfig.StandardTrackFormat = "{Release Group}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(_trackFile.ReleaseGroup); } @@ -443,73 +344,59 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests public void should_be_able_to_use_original_title() { _artist.Name = "Linkin Park"; - _namingConfig.StandardTrackFormat = "{Artist Name} - {Original Title} - {Track Title}"; + _namingConfig.StandardTrackFormat = "{Artist Name} - {Original Title}"; _trackFile.SceneName = "Linkin.Park.Meteora.320-LOL"; _trackFile.Path = "30 Rock - 01 - Test"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("Linkin Park - Linkin.Park.Meteora.320-LOL - City Sushi"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("Linkin Park - Linkin.Park.Meteora.320-LOL"); } [Test] public void should_replace_double_period_with_single_period() { - _namingConfig.StandardTrackFormat = "{Artist.Name}.{track:00}.{Track.Title}"; + _namingConfig.StandardTrackFormat = "{Artist.Name}.{Album.Title}"; - var track = Builder.CreateNew() - .With(e => e.Title = "Part 1") - .With(e => e.AbsoluteTrackNumber = 6) - .With(e => e.AlbumRelease = _release) - .Build(); - - Subject.BuildTrackFileName(new List { track }, new Artist { Name = "In The Woods." }, new Album { Title = "30 Rock" }, _trackFile) - .Should().Be("In.The.Woods.06.Part.1"); + Subject.BuildTrackFileName(new Author { Name = "In The Woods." }, new Book { Title = "30 Rock" }, _trackFile) + .Should().Be("In.The.Woods.30.Rock"); } [Test] public void should_replace_triple_period_with_single_period() { - _namingConfig.StandardTrackFormat = "{Artist.Name}.{track:00}.{Track.Title}"; - - var track = Builder.CreateNew() - .With(e => e.Title = "Part 1") - .With(e => e.AbsoluteTrackNumber = 6) - .With(e => e.AlbumRelease = _release) - .Build(); + _namingConfig.StandardTrackFormat = "{Artist.Name}.{Album.Title}"; - Subject.BuildTrackFileName(new List { track }, new Artist { Name = "In The Woods..." }, new Album { Title = "30 Rock" }, _trackFile) - .Should().Be("In.The.Woods.06.Part.1"); + Subject.BuildTrackFileName(new Author { Name = "In The Woods..." }, new Book { Title = "30 Rock" }, _trackFile) + .Should().Be("In.The.Woods.30.Rock"); } [Test] public void should_include_affixes_if_value_not_empty() { - _namingConfig.StandardTrackFormat = "{Artist.Name}.{track:00}{_Track.Title_}{Quality.Title}"; + _namingConfig.StandardTrackFormat = "{Artist.Name}{_Album.Title_}{Quality.Title}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("Linkin.Park.06_City.Sushi_MP3-256"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("Linkin.Park_Hybrid.Theory_MP3-320"); } [Test] public void should_not_include_affixes_if_value_empty() { - _namingConfig.StandardTrackFormat = "{Artist.Name}.{track:00}{_Track.Title_}"; - - _track1.Title = ""; + _namingConfig.StandardTrackFormat = "{Artist.Name}{_Album.Title_}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("Linkin.Park.06"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("Linkin.Park_Hybrid.Theory"); } [Test] public void should_remove_duplicate_non_word_characters() { _artist.Name = "Venture Bros."; - _namingConfig.StandardTrackFormat = "{Artist.Name}.{Album.Title}-{track:00}"; + _namingConfig.StandardTrackFormat = "{Artist.Name}.{Album.Title}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("Venture.Bros.Hybrid.Theory-06"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("Venture.Bros.Hybrid.Theory"); } [Test] @@ -521,7 +408,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _trackFile.SceneName = null; _trackFile.Path = "existing.file.mkv"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path)); } @@ -534,7 +421,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _trackFile.SceneName = "30.Rock.S01E01.xvid-LOL"; _trackFile.Path = "30 Rock - S01E01 - Test"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("30.Rock.S01E01.xvid-LOL"); } @@ -543,26 +430,26 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = "{Quality Title} {Quality Proper}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("MP3-256"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("MP3-320"); } [Test] public void should_not_wrap_proper_in_square_brackets_when_not_a_proper() { - _namingConfig.StandardTrackFormat = "{Artist Name} - {track:00} [{Quality Title}] {[Quality Proper]}"; + _namingConfig.StandardTrackFormat = "{Artist Name} - {Album Title} [{Quality Title}] {[Quality Proper]}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("Linkin Park - 06 [MP3-256]"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("Linkin Park - Hybrid Theory [MP3-320]"); } [Test] public void should_replace_quality_full_with_quality_title_only_when_not_a_proper() { - _namingConfig.StandardTrackFormat = "{Artist Name} - {track:00} [{Quality Full}]"; + _namingConfig.StandardTrackFormat = "{Artist Name} - {Album Title} [{Quality Full}]"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("Linkin Park - 06 [MP3-256]"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("Linkin Park - Hybrid Theory [MP3-320]"); } [TestCase(' ')] @@ -573,8 +460,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { _namingConfig.StandardTrackFormat = string.Format("{{Quality{0}Title}}{0}{{Quality{0}Proper}}", separator); - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be("MP3-256"); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be("MP3-320"); } [TestCase(' ')] @@ -583,10 +470,10 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests [TestCase('_')] public void should_trim_extra_separators_from_middle_when_quality_proper_is_not_included(char separator) { - _namingConfig.StandardTrackFormat = string.Format("{{Quality{0}Title}}{0}{{Quality{0}Proper}}{0}{{Track{0}Title}}", separator); + _namingConfig.StandardTrackFormat = string.Format("{{Quality{0}Title}}{0}{{Quality{0}Proper}}{0}{{Album{0}Title}}", separator); - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be(string.Format("MP3-256{0}City{0}Sushi", separator)); + Subject.BuildTrackFileName(_artist, _album, _trackFile) + .Should().Be(string.Format("MP3-320{0}Hybrid{0}Theory", separator)); } [Test] @@ -598,7 +485,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _trackFile.SceneName = "30.Rock.S01E01.xvid-LOL"; _trackFile.Path = "30 Rock - S01E01 - Test"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("30 Rock - 30 Rock - S01E01 - Test"); } @@ -611,7 +498,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _trackFile.SceneName = "30.Rock.S01E01.xvid-LOL"; _trackFile.Path = "30 Rock - S01E01 - Test"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("30 Rock - S01E01 - Test"); } @@ -621,19 +508,19 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _trackFile.ReleaseGroup = null; _namingConfig.StandardTrackFormat = "{Release Group}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be("Readarr"); } - [TestCase("{Track Title}{-Release Group}", "City Sushi")] - [TestCase("{Track Title}{ Release Group}", "City Sushi")] - [TestCase("{Track Title}{ [Release Group]}", "City Sushi")] + [TestCase("{Album Title}{-Release Group}", "Hybrid Theory")] + [TestCase("{Album Title}{ Release Group}", "Hybrid Theory")] + [TestCase("{Album Title}{ [Release Group]}", "Hybrid Theory")] public void should_not_use_Readarr_as_release_group_if_pattern_has_separator(string pattern, string expectedFileName) { _trackFile.ReleaseGroup = null; _namingConfig.StandardTrackFormat = pattern; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(expectedFileName); } @@ -645,7 +532,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _trackFile.ReleaseGroup = releaseGroup; _namingConfig.StandardTrackFormat = "{Release Group}"; - Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(releaseGroup); } } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs index 2bff4ea1c..71b4675c4 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs @@ -14,38 +14,25 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests [TestFixture] public class TitleTheFixture : CoreTest { - private Artist _artist; - private Album _album; - private AlbumRelease _release; - private Track _track; - private TrackFile _trackFile; + private Author _artist; + private Book _album; + private BookFile _trackFile; private NamingConfig _namingConfig; [SetUp] public void Setup() { - _artist = Builder + _artist = Builder .CreateNew() .With(s => s.Name = "Alien Ant Farm") .Build(); - _album = Builder + _album = Builder .CreateNew() .With(s => s.Title = "Anthology") .Build(); - _release = Builder - .CreateNew() - .With(s => s.Media = new List { new Medium { Number = 1 } }) - .Build(); - - _track = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.AbsoluteTrackNumber = 6) - .With(e => e.AlbumRelease = _release) - .Build(); - - _trackFile = new TrackFile { Quality = new QualityModel(Quality.MP3_320), ReleaseGroup = "ReadarrTest" }; + _trackFile = new BookFile { Quality = new QualityModel(Quality.MP3_320), ReleaseGroup = "ReadarrTest" }; _namingConfig = NamingConfig.Default; _namingConfig.RenameTracks = true; @@ -75,7 +62,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _artist.Name = name; _namingConfig.StandardTrackFormat = "{Artist NameThe}"; - Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(expected); } @@ -88,7 +75,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _artist.Name = name; _namingConfig.StandardTrackFormat = "{Artist NameThe}"; - Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + Subject.BuildTrackFileName(_artist, _album, _trackFile) .Should().Be(name); } } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetAlbumFolderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetAlbumFolderFixture.cs deleted file mode 100644 index 359b016b5..000000000 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetAlbumFolderFixture.cs +++ /dev/null @@ -1,35 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Music; -using NzbDrone.Core.Organizer; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.OrganizerTests -{ - [TestFixture] - public class GetAlbumFolderFixture : CoreTest - { - private NamingConfig _namingConfig; - - [SetUp] - public void Setup() - { - _namingConfig = NamingConfig.Default; - - Mocker.GetMock() - .Setup(c => c.GetConfig()).Returns(_namingConfig); - } - - [TestCase("Venture Bros.", "Today", "{Artist.Name}.{Album.Title}", "Venture.Bros.Today")] - [TestCase("Venture Bros.", "Today", "{Artist Name} {Album Title}", "Venture Bros. Today")] - public void should_use_albumFolderFormat_to_build_folder_name(string artistName, string albumTitle, string format, string expected) - { - _namingConfig.AlbumFolderFormat = format; - - var artist = new Artist { Name = artistName }; - var album = new Album { Title = albumTitle }; - - Subject.GetAlbumFolder(artist, album, _namingConfig).Should().Be(expected); - } - } -} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetArtistFolderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetArtistFolderFixture.cs index 0c84ddd7a..933159126 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetArtistFolderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/GetArtistFolderFixture.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.OrganizerTests { _namingConfig.ArtistFolderFormat = format; - var artist = new Artist { Name = artistName }; + var artist = new Author { Name = artistName }; Subject.GetArtistFolder(artist).Should().Be(expected); } diff --git a/src/NzbDrone.Core.Test/ParserTests/ExtendedQualityParserRegex.cs b/src/NzbDrone.Core.Test/ParserTests/ExtendedQualityParserRegex.cs index e7007a4cb..7f13d78f4 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ExtendedQualityParserRegex.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ExtendedQualityParserRegex.cs @@ -24,10 +24,10 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The Real Housewives of Some Place - S01E01 - Why are we doing this?", 0)] public void should_parse_reality_from_title(string title, int reality) { - QualityParser.ParseQuality(title, null, 0).Revision.Real.Should().Be(reality); + QualityParser.ParseQuality(title).Revision.Real.Should().Be(reality); } - [TestCase("Chuck.S04E05.HDTV.XviD-LOL", 1)] + [TestCase("Chuck.S04E05.HDTV.XviD-LOL", 0)] [TestCase("Gold.Rush.S04E05.Garnets.or.Gold.REAL.REAL.PROPER.HDTV.x264-W4F", 2)] [TestCase("Chuck.S03E17.REAL.PROPER.720p.HDTV.x264-ORENJI-RP", 2)] [TestCase("Covert.Affairs.S05E09.REAL.PROPER.HDTV.x264-KILLERS", 2)] @@ -38,14 +38,13 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("House.S07E11.PROPER.REAL.RERIP.1080p.BluRay.x264-TENEIGHTY", 2)] [TestCase("[MGS] - Kuragehime - Episode 02v2 - [D8B6C90D]", 2)] [TestCase("[Hatsuyuki] Tokyo Ghoul - 07 [v2][848x480][23D8F455].avi", 2)] - [TestCase("[DeadFish] Barakamon - 01v3 [720p][AAC]", 3)] [TestCase("[DeadFish] Momo Kyun Sword - 01v4 [720p][AAC]", 4)] [TestCase("[Vivid-Asenshi] Akame ga Kill - 04v2 [266EE983]", 2)] [TestCase("[Vivid-Asenshi] Akame ga Kill - 03v2 [66A05817]", 2)] [TestCase("[Vivid-Asenshi] Akame ga Kill - 02v2 [1F67AB55]", 2)] public void should_parse_version_from_title(string title, int version) { - QualityParser.ParseQuality(title, null, 0).Revision.Version.Should().Be(version); + QualityParser.ParseQuality(title).Revision.Version.Should().Be(version); } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs b/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs index ad53c6890..66abce3ad 100644 --- a/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs @@ -15,70 +15,70 @@ namespace NzbDrone.Core.Test.ParserTests { @"C:\Test\Some.Hashed.Release.(256kbps)-Mercury\0e895c37245186812cb08aab1529cf8ee389dd05.mp3".AsOsAgnostic(), "Some Hashed Release", - Quality.MP3_256, + Quality.MP3_320, "Mercury" }, new object[] { @"C:\Test-[256]\0e895c37245186812cb08aab1529cf8ee389dd05\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury.mp3".AsOsAgnostic(), "Some Hashed Release", - Quality.MP3_256, + Quality.MP3_320, "Mercury" }, new object[] { @"C:\Test\Fake.Dir.S01E01-Test\yrucreM-462.H.0.2CAA.LD-BEW.p027.10E10S.esaeleR.dehsaH.emoS.mp3".AsOsAgnostic(), "Some Hashed Release", - Quality.MP3_256, + Quality.MP3_320, "Mercury" }, new object[] { @"C:\Test\Fake.Dir.S01E01-Test\yrucreM-LN 1.5DD LD-BEW P0801 10E10S esaeleR dehsaH emoS.mp3".AsOsAgnostic(), "Some Hashed Release", - Quality.MP3_256, + Quality.MP3_320, "Mercury" }, new object[] { @"C:\Test\Weeds.S01E10.DVDRip.XviD-Readarr\AHFMZXGHEWD660.mp3".AsOsAgnostic(), "Weeds", - Quality.MP3_256, + Quality.MP3_320, "Readarr" }, new object[] { @"C:\Test\Deadwood.S02E12.1080p.BluRay.x264-Readarr\Backup_72023S02-12.mp3".AsOsAgnostic(), "Deadwood", - Quality.MP3_256, + Quality.MP3_320, null }, new object[] { @"C:\Test\Grimm S04E08 Chupacabra 720p WEB-DL DD5 1 H 264-ECI\123.mp3".AsOsAgnostic(), "Grimm", - Quality.MP3_256, + Quality.MP3_320, "ECI" }, new object[] { @"C:\Test\Grimm S04E08 Chupacabra 720p WEB-DL DD5 1 H 264-ECI\abc.mp3".AsOsAgnostic(), "Grimm", - Quality.MP3_256, + Quality.MP3_320, "ECI" }, new object[] { @"C:\Test\Grimm S04E08 Chupacabra 720p WEB-DL DD5 1 H 264-ECI\b00bs.mp3".AsOsAgnostic(), "Grimm", - Quality.MP3_256, + Quality.MP3_320, "ECI" }, new object[] { @"C:\Test\The.Good.Wife.S02E23.720p.HDTV.x264-NZBgeek/cgajsofuejsa501.mp3".AsOsAgnostic(), "The Good Wife", - Quality.MP3_256, + Quality.MP3_320, "NZBgeek" } }; diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 921d1246f..2a317c860 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -12,16 +12,16 @@ namespace NzbDrone.Core.Test.ParserTests [TestFixture] public class ParserFixture : CoreTest { - private Artist _artist = new Artist(); - private List _albums = new List { new Album() }; + private Author _artist = new Author(); + private List _albums = new List { new Book() }; [SetUp] public void Setup() { - _artist = Builder + _artist = Builder .CreateNew() .Build(); - _albums = Builder> + _albums = Builder> .CreateNew() .Build(); } @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.ParserTests private void GivenSearchCriteria(string artistName, string albumTitle) { _artist.Name = artistName; - var a = new Album(); + var a = new Book(); a.Title = albumTitle; _albums.Add(a); } diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetAlbumsFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetAlbumsFixture.cs index 5973e1b8f..3d2de1244 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetAlbumsFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetAlbumsFixture.cs @@ -18,8 +18,8 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests [Test] public void should_not_fail_if_search_criteria_contains_multiple_albums_with_the_same_name() { - var artist = Builder.CreateNew().Build(); - var albums = Builder.CreateListOfSize(2).All().With(x => x.Title = "IdenticalTitle").Build().ToList(); + var artist = Builder.CreateNew().Build(); + var albums = Builder.CreateListOfSize(2).All().With(x => x.Title = "IdenticalTitle").Build().ToList(); var criteria = new AlbumSearchCriteria { Artist = artist, @@ -31,10 +31,10 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests AlbumTitle = "IdenticalTitle" }; - Subject.GetAlbums(parsed, artist, criteria).Should().BeEquivalentTo(new List()); + Subject.GetAlbums(parsed, artist, criteria).Should().BeEquivalentTo(new List()); Mocker.GetMock() - .Verify(s => s.FindByTitle(artist.ArtistMetadataId, "IdenticalTitle"), Times.Once()); + .Verify(s => s.FindByTitle(artist.AuthorMetadataId, "IdenticalTitle"), Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 98ffd96be..685201063 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -12,240 +12,49 @@ namespace NzbDrone.Core.Test.ParserTests { public static object[] SelfQualityParserCases = { - new object[] { Quality.MP3_192 }, - new object[] { Quality.MP3_VBR }, - new object[] { Quality.MP3_256 }, new object[] { Quality.MP3_320 }, - new object[] { Quality.MP3_VBR_V2 }, - new object[] { Quality.WAV }, - new object[] { Quality.WMA }, - new object[] { Quality.AAC_192 }, - new object[] { Quality.AAC_256 }, - new object[] { Quality.AAC_320 }, - new object[] { Quality.AAC_VBR }, - new object[] { Quality.ALAC }, new object[] { Quality.FLAC }, + new object[] { Quality.EPUB }, + new object[] { Quality.MOBI }, + new object[] { Quality.AZW3 } }; - [TestCase("", "MPEG Version 1 Audio, Layer 3", 96)] - public void should_parse_mp3_96_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.MP3_096); - } - - [TestCase("", "MPEG Version 1 Audio, Layer 3", 128)] - public void should_parse_mp3_128_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.MP3_128); - } - - [TestCase("", "MPEG Version 1 Audio, Layer 3", 160)] - public void should_parse_mp3_160_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.MP3_160); - } - - [TestCase("VA - The Best 101 Love Ballads (2017) MP3 [192 kbps]", null, 0)] - [TestCase("ATCQ - The Love Movement 1998 2CD 192kbps RIP", null, 0)] - [TestCase("A Tribe Called Quest - The Love Movement 1998 2CD [192kbps] RIP", null, 0)] - [TestCase("Maula - Jism 2 [2012] Mp3 - 192Kbps [Extended]- TK", null, 0)] - [TestCase("VA - Complete Clubland - The Ultimate Ride Of Your Lfe [2014][MP3][192 kbps]", null, 0)] - [TestCase("Complete Clubland - The Ultimate Ride Of Your Lfe [2014][MP3](192kbps)", null, 0)] - [TestCase("The Ultimate Ride Of Your Lfe [192 KBPS][2014][MP3]", null, 0)] - [TestCase("Gary Clark Jr - Live North America 2016 (2017) MP3 192kbps", null, 0)] - [TestCase("Some Song [192][2014][MP3]", null, 0)] - [TestCase("Other Song (192)[2014][MP3]", null, 0)] - [TestCase("", "MPEG Version 1 Audio, Layer 3", 192)] - public void should_parse_mp3_192_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.MP3_192); - } - - [TestCase("Caetano Veloso Discografia Completa MP3 @256", null, 0)] - [TestCase("Ricky Martin - A Quien Quiera Escuchar (2015) 256 kbps [GloDLS]", null, 0)] - [TestCase("Jake Bugg - Jake Bugg (Album) [2012] {MP3 256 kbps}", null, 0)] - [TestCase("Clean Bandit - New Eyes [2014] [Mp3-256]-V3nom [GLT]", null, 0)] - [TestCase("Armin van Buuren - A State Of Trance 810 (20.04.2017) 256 kbps", null, 0)] - [TestCase("PJ Harvey - Let England Shake [mp3-256-2011][trfkad]", null, 0)] - [TestCase("", "MPEG Version 1 Audio, Layer 3", 256)] - public void should_parse_mp3_256_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.MP3_256); - } - - [TestCase("Beyoncé Lemonade [320] 2016 Beyonce Lemonade [320] 2016", null, 0)] - [TestCase("Childish Gambino - Awaken, My Love Album 2016 mp3 320 Kbps", null, 0)] - [TestCase("Maluma – Felices Los 4 MP3 320 Kbps 2017 Download", null, 0)] - [TestCase("Ricardo Arjona - APNEA (Single 2014) (320 kbps)", null, 0)] - [TestCase("Kehlani - SweetSexySavage (Deluxe Edition) (2017) 320", null, 0)] - [TestCase("Anderson Paak - Malibu (320)(2016)", null, 0)] - [TestCase("", "MPEG Version 1 Audio, Layer 3", 320)] - public void should_parse_mp3_320_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.MP3_320); - } - - [TestCase("Sia - This Is Acting (Standard Edition) [2016-Web-MP3-V0(VBR)]", null, 0)] - [TestCase("Mount Eerie - A Crow Looked at Me (2017) [MP3 V0 VBR)]", null, 0)] - public void should_parse_mp3_vbr_v0_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.MP3_VBR); - } - - //TODO Parser should look at bitrate range for quality to determine level of VBR - [TestCase("", "MPEG Version 1 Audio, Layer 3 VBR", 298)] - [Ignore("Parser should look at bitrate range for quality to determine level of VBR")] - public void should_parse_mp3_vbr_v2_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.MP3_VBR_V2); - } - - [TestCase("Kendrick Lamar - DAMN (2017) FLAC", null, 0)] - [TestCase("Alicia Keys - Vault Playlist Vol. 1 (2017) [FLAC CD]", null, 0)] - [TestCase("Gorillaz - Humanz (Deluxe) - lossless FLAC Tracks - 2017 - CDrip", null, 0)] - [TestCase("David Bowie - Blackstar (2016) [FLAC]", null, 0)] - [TestCase("The Cure - Greatest Hits (2001) FLAC Soup", null, 0)] - [TestCase("Slowdive- Souvlaki (FLAC)", null, 0)] - [TestCase("John Coltrane - Kulu Se Mama (1965) [EAC-FLAC]", null, 0)] - [TestCase("The Rolling Stones - The Very Best Of '75-'94 (1995) {FLAC}", null, 0)] - [TestCase("Migos-No_Label_II-CD-FLAC-2014-FORSAKEN", null, 0)] - [TestCase("ADELE 25 CD FLAC 2015 PERFECT", null, 0)] - [TestCase("", "Flac Audio", 1057)] - public void should_parse_flac_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.FLAC); - } - - [TestCase("Beck.-.Guero.2005.[2016.Remastered].24bit.96kHz.LOSSLESS.FLAC", null, 0, 0)] - [TestCase("[R.E.M - Lifes Rich Pageant(1986) [24bit192kHz 2016 Remaster]LOSSLESS FLAC]", null, 0, 0)] - [TestCase("", "Flac Audio", 5057, 24)] - public void should_parse_flac_24bit_quality(string title, string desc, int bitrate, int sampleSize) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.FLAC_24, sampleSize); - } - - [TestCase("", "Microsoft WMA2 Audio", 218)] - public void should_parse_wma_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.WMA); - } - - [TestCase("", "PCM Audio", 1411)] - public void should_parse_wav_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.WAV); - } - - [TestCase("Chuck Berry Discography ALAC", null, 0)] - [TestCase("A$AP Rocky - LONG LIVE A$AP Deluxe asap[ALAC]", null, 0)] - [TestCase("", "MPEG-4 Audio (alac)", 0)] - public void should_parse_alac_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.ALAC); - } - - [TestCase("Stevie Ray Vaughan Discography (1981-1987) [APE]", null, 0)] - [TestCase("Brain Ape - Rig it [2014][ape]", null, 0)] - [TestCase("", "Monkey's Audio", 0)] - public void should_parse_ape_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.APE); - } - - [TestCase("Arctic Monkeys - AM {2013-Album}", null, 0)] - [TestCase("Audio Adrinaline - Audio Adrinaline", null, 0)] - [TestCase("Audio Adrinaline - Audio Adrinaline [Mixtape FLAC]", null, 0)] - [TestCase("Brain Ape - Rig it [2014][flac]", null, 0)] - [TestCase("Coil - The Ape Of Naples(2005) (FLAC)", null, 0)] - public void should_not_parse_ape_quality(string title, string desc, int bitrate) - { - var result = QualityParser.ParseQuality(title, desc, bitrate); - result.Quality.Should().NotBe(Quality.APE); - } - - [TestCase("Opus - Drums Unlimited (1966) [Flac]", null, 0)] - public void should_not_parse_opus_quality(string title, string desc, int bitrate) - { - var result = QualityParser.ParseQuality(title, desc, bitrate); - result.Quality.Should().Be(Quality.FLAC); - } - - [TestCase("Max Roach - Drums Unlimited (1966) [WavPack]", null, 0)] - [TestCase("Roxette - Charm School(2011) (2CD) [WV]", null, 0)] - [TestCase("", "WavPack", 0)] - public void should_parse_wavpack_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.WAVPACK); - } - - [TestCase("Milky Chance - Sadnecessary [256 Kbps] [M4A]", null, 0)] - [TestCase("Little Mix - Salute [Deluxe Edition] [2013] [M4A-256]-V3nom [GLT", null, 0)] - [TestCase("X-Men Soundtracks (2006-2014) AAC, 256 kbps", null, 0)] - [TestCase("The Weeknd - The Hills - Single[iTunes Plus AAC M4A]", null, 0)] - [TestCase("Walk the Line Soundtrack (2005) [AAC, 256 kbps]", null, 0)] - [TestCase("Firefly Soundtrack(2007 (2002-2003)) [AAC, 256 kbps VBR]", null, 0)] - public void should_parse_aac_256_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.AAC_256); - } - - [TestCase("", "MPEG-4 Audio (mp4a)", 320)] - [TestCase("", "MPEG-4 Audio (drms)", 320)] - public void should_parse_aac_320_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.AAC_320); - } - - [TestCase("", "MPEG-4 Audio (mp4a)", 321)] - [TestCase("", "MPEG-4 Audio (drms)", 321)] - public void should_parse_aac_vbr_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.AAC_VBR); - } - - [TestCase("Kirlian Camera - The Ice Curtain - Album 1998 - Ogg-Vorbis Q10", null, 0)] - [TestCase("", "Vorbis Version 0 Audio", 500)] - [TestCase("", "Opus Version 1 Audio", 501)] - public void should_parse_vorbis_q10_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q10); - } - - [TestCase("", "Vorbis Version 0 Audio", 320)] - [TestCase("", "Opus Version 1 Audio", 321)] - public void should_parse_vorbis_q9_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q9); - } - - [TestCase("Various Artists - No New York [1978/Ogg/q8]", null, 0)] - [TestCase("", "Vorbis Version 0 Audio", 256)] - [TestCase("", "Opus Version 1 Audio", 257)] - public void should_parse_vorbis_q8_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q8); - } - - [TestCase("Masters_At_Work-Nuyorican_Soul-.Talkin_Loud.-1997-OGG.Q7", null, 0)] - [TestCase("", "Vorbis Version 0 Audio", 224)] - [TestCase("", "Opus Version 1 Audio", 225)] - public void should_parse_vorbis_q7_quality(string title, string desc, int bitrate) - { - ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q7); - } - - [TestCase("", "Vorbis Version 0 Audio", 192)] - [TestCase("", "Opus Version 1 Audio", 193)] - public void should_parse_vorbis_q6_quality(string title, string desc, int bitrate) + [TestCase("VA - The Best 101 Love Ballads (2017) MP3 [192 kbps]")] + [TestCase("Maula - Jism 2 [2012] Mp3 - 192Kbps [Extended]- TK")] + [TestCase("VA - Complete Clubland - The Ultimate Ride Of Your Lfe [2014][MP3][192 kbps]")] + [TestCase("Complete Clubland - The Ultimate Ride Of Your Lfe [2014][MP3](192kbps)")] + [TestCase("The Ultimate Ride Of Your Lfe [192 KBPS][2014][MP3]")] + [TestCase("Gary Clark Jr - Live North America 2016 (2017) MP3 192kbps")] + [TestCase("Some Song [192][2014][MP3]")] + [TestCase("Other Song (192)[2014][MP3]")] + [TestCase("Caetano Veloso Discografia Completa MP3 @256")] + [TestCase("Jake Bugg - Jake Bugg (Album) [2012] {MP3 256 kbps}")] + [TestCase("Clean Bandit - New Eyes [2014] [Mp3-256]-V3nom [GLT]")] + [TestCase("PJ Harvey - Let England Shake [mp3-256-2011][trfkad]")] + [TestCase("Childish Gambino - Awaken, My Love Album 2016 mp3 320 Kbps")] + [TestCase("Maluma – Felices Los 4 MP3 320 Kbps 2017 Download")] + [TestCase("Sia - This Is Acting (Standard Edition) [2016-Web-MP3-V0(VBR)]")] + [TestCase("Mount Eerie - A Crow Looked at Me (2017) [MP3 V0 VBR)]")] + [TestCase("Queen - The Ultimate Best Of Queen(2011)[mp3]")] + [TestCase("Maroon 5 Ft Kendrick Lamar -Dont Wanna Know MP3 2016")] + public void should_parse_mp3_quality(string title) { - ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q6); + ParseAndVerifyQuality(title, null, 0, Quality.MP3_320); } - [TestCase("", "Vorbis Version 0 Audio", 160)] - [TestCase("", "Opus Version 1 Audio", 161)] - public void should_parse_vorbis_q5_quality(string title, string desc, int bitrate) + [TestCase("Kendrick Lamar - DAMN (2017) FLAC")] + [TestCase("Alicia Keys - Vault Playlist Vol. 1 (2017) [FLAC CD]")] + [TestCase("Gorillaz - Humanz (Deluxe) - lossless FLAC Tracks - 2017 - CDrip")] + [TestCase("David Bowie - Blackstar (2016) [FLAC]")] + [TestCase("The Cure - Greatest Hits (2001) FLAC Soup")] + [TestCase("Slowdive- Souvlaki (FLAC)")] + [TestCase("John Coltrane - Kulu Se Mama (1965) [EAC-FLAC]")] + [TestCase("The Rolling Stones - The Very Best Of '75-'94 (1995) {FLAC}")] + [TestCase("Migos-No_Label_II-CD-FLAC-2014-FORSAKEN")] + [TestCase("ADELE 25 CD FLAC 2015 PERFECT")] + public void should_parse_flac_quality(string title) { - ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q5); + ParseAndVerifyQuality(title, null, 0, Quality.FLAC); } // Flack doesn't get match for 'FLAC' quality @@ -257,11 +66,6 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The Chainsmokers & Coldplay - Something Just Like This")] [TestCase("Frank Ocean Blonde 2016")] - - //TODO: This should be parsed as Unknown and not MP3-96 - //[TestCase("A - NOW Thats What I Call Music 96 (2017) [Mp3~Kbps]")] - [TestCase("Queen - The Ultimate Best Of Queen(2011)[mp3]")] - [TestCase("Maroon 5 Ft Kendrick Lamar -Dont Wanna Know MP3 2016")] public void quality_parse(string title) { ParseAndVerifyQuality(title, null, 0, Quality.Unknown); @@ -272,26 +76,14 @@ namespace NzbDrone.Core.Test.ParserTests public void parsing_our_own_quality_enum_name(Quality quality) { var fileName = string.Format("Some album [{0}]", quality.Name); - var result = QualityParser.ParseQuality(fileName, null, 0); + var result = QualityParser.ParseQuality(fileName); result.Quality.Should().Be(quality); } [TestCase("Little Mix - Salute [Deluxe Edition] [2013] [M4A-256]-V3nom [GLT")] public void should_parse_quality_from_name(string title) { - QualityParser.ParseQuality(title, null, 0).QualityDetectionSource.Should().Be(QualityDetectionSource.Name); - } - - [TestCase("01. Kanye West - Ultralight Beam.mp3")] - [TestCase("01. Kanye West - Ultralight Beam.ogg")] - - //These get detected by name as we are looking for the extensions as identifiers for release names - //[TestCase("01. Kanye West - Ultralight Beam.m4a")] - //[TestCase("01. Kanye West - Ultralight Beam.wma")] - //[TestCase("01. Kanye West - Ultralight Beam.wav")] - public void should_parse_quality_from_extension(string title) - { - QualityParser.ParseQuality(title, null, 0).QualityDetectionSource.Should().Be(QualityDetectionSource.Extension); + QualityParser.ParseQuality(title).QualityDetectionSource.Should().Be(QualityDetectionSource.Name); } [Test] @@ -305,14 +97,14 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Artist Title - Album Title 2017 PROPER FLAC aAF", false)] public void should_be_able_to_parse_repack(string title, bool isRepack) { - var result = QualityParser.ParseQuality(title, null, 0); + var result = QualityParser.ParseQuality(title); result.Revision.Version.Should().Be(2); result.Revision.IsRepack.Should().Be(isRepack); } private void ParseAndVerifyQuality(string name, string desc, int bitrate, Quality quality, int sampleSize = 0) { - var result = QualityParser.ParseQuality(name, desc, bitrate, sampleSize); + var result = QualityParser.ParseQuality(name); result.Quality.Should().Be(quality); } } diff --git a/src/NzbDrone.Core.Test/Profiles/Metadata/MetadataProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Profiles/Metadata/MetadataProfileRepositoryFixture.cs index 5601eee4c..db24a0a15 100644 --- a/src/NzbDrone.Core.Test/Profiles/Metadata/MetadataProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/Metadata/MetadataProfileRepositoryFixture.cs @@ -13,36 +13,7 @@ namespace NzbDrone.Core.Test.Profiles.Metadata [Test] public void should_be_able_to_read_and_write() { - var profile = new MetadataProfile - { - PrimaryAlbumTypes = PrimaryAlbumType.All.OrderByDescending(l => l.Name).Select(l => new ProfilePrimaryAlbumTypeItem - { - PrimaryAlbumType = l, - Allowed = l == PrimaryAlbumType.Album - }).ToList(), - - SecondaryAlbumTypes = SecondaryAlbumType.All.OrderByDescending(l => l.Name).Select(l => new ProfileSecondaryAlbumTypeItem - { - SecondaryAlbumType = l, - Allowed = l == SecondaryAlbumType.Studio - }).ToList(), - - ReleaseStatuses = ReleaseStatus.All.OrderByDescending(l => l.Name).Select(l => new ProfileReleaseStatusItem - { - ReleaseStatus = l, - Allowed = l == ReleaseStatus.Official - }).ToList(), - - Name = "TestProfile" - }; - - Subject.Insert(profile); - - StoredModel.Name.Should().Be(profile.Name); - - StoredModel.PrimaryAlbumTypes.Should().Equal(profile.PrimaryAlbumTypes, (a, b) => a.PrimaryAlbumType == b.PrimaryAlbumType && a.Allowed == b.Allowed); - StoredModel.SecondaryAlbumTypes.Should().Equal(profile.SecondaryAlbumTypes, (a, b) => a.SecondaryAlbumType == b.SecondaryAlbumType && a.Allowed == b.Allowed); - StoredModel.ReleaseStatuses.Should().Equal(profile.ReleaseStatuses, (a, b) => a.ReleaseStatus == b.ReleaseStatus && a.Allowed == b.Allowed); + // TODO: restore } } } diff --git a/src/NzbDrone.Core.Test/Profiles/Metadata/MetadataProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Profiles/Metadata/MetadataProfileServiceFixture.cs index 00c40579f..4daefc516 100644 --- a/src/NzbDrone.Core.Test/Profiles/Metadata/MetadataProfileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/Metadata/MetadataProfileServiceFixture.cs @@ -61,14 +61,6 @@ namespace NzbDrone.Core.Test.Profiles.Metadata var profiles = Builder.CreateListOfSize(2) .TheFirst(1) .With(x => x.Name = MetadataProfileService.NONE_PROFILE_NAME) - .With(x => x.PrimaryAlbumTypes = new List - { - new ProfilePrimaryAlbumTypeItem - { - PrimaryAlbumType = PrimaryAlbumType.Album, - Allowed = true - } - }) .BuildList(); Mocker.GetMock() @@ -113,7 +105,7 @@ namespace NzbDrone.Core.Test.Profiles.Metadata .With(p => p.Id = 2) .Build(); - var artistList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .Random(1) .With(c => c.MetadataProfileId = profile.Id) .Build().ToList(); @@ -145,7 +137,7 @@ namespace NzbDrone.Core.Test.Profiles.Metadata .With(p => p.Id = 2) .Build(); - var artistList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .All() .With(c => c.MetadataProfileId = 1) .Build().ToList(); @@ -177,7 +169,7 @@ namespace NzbDrone.Core.Test.Profiles.Metadata .With(p => p.Id = 2) .Build(); - var artistList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .All() .With(c => c.MetadataProfileId = 1) .Build().ToList(); @@ -209,7 +201,7 @@ namespace NzbDrone.Core.Test.Profiles.Metadata .With(p => p.Id = 1) .Build(); - var artistList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .All() .With(c => c.MetadataProfileId = 2) .Build().ToList(); diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs index 1d6fbb587..0c26d57df 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Test.Profiles { var profile = new QualityProfile { - Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_320, Quality.MP3_192, Quality.MP3_256), + Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_320, Quality.MP3_320, Quality.MP3_320), Cutoff = Quality.MP3_320.Id, Name = "TestProfile" }; diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs index 4044f658f..3e756e3ff 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.Profiles Subject.Handle(new ApplicationStartedEvent()); Mocker.GetMock() - .Verify(v => v.Insert(It.IsAny()), Times.Exactly(3)); + .Verify(v => v.Insert(It.IsAny()), Times.Exactly(4)); } [Test] @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.Profiles .With(p => p.Id = 2) .Build(); - var artistList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .Random(1) .With(c => c.QualityProfileId = profile.Id) .Build().ToList(); @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.Profiles .With(p => p.Id = 2) .Build(); - var artistList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .All() .With(c => c.QualityProfileId = 1) .Build().ToList(); @@ -111,7 +111,7 @@ namespace NzbDrone.Core.Test.Profiles .With(p => p.Id = 2) .Build(); - var artistList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .All() .With(c => c.QualityProfileId = 1) .Build().ToList(); @@ -139,7 +139,7 @@ namespace NzbDrone.Core.Test.Profiles [Test] public void should_delete_profile_if_not_assigned_to_artist_import_list_or_root_folder() { - var artistList = Builder.CreateListOfSize(3) + var artistList = Builder.CreateListOfSize(3) .All() .With(c => c.QualityProfileId = 2) .Build().ToList(); diff --git a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs index 7fc62c5c6..6586f5fd5 100644 --- a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs @@ -13,14 +13,14 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService [TestFixture] public class CalculateFixture : CoreTest { - private Artist _artist = null; + private Author _artist = null; private List _releaseProfiles = null; private string _title = "Artist.Name-Album.Title.2018.FLAC.24bit-Readarr"; [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(s => s.Tags = new HashSet(new[] { 1, 2 })) .Build(); diff --git a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs index 9a2f2bd12..d78f3ac94 100644 --- a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs @@ -13,14 +13,14 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService [TestFixture] public class GetMatchingPreferredWordsFixture : CoreTest { - private Artist _artist = null; + private Author _artist = null; private List _releaseProfiles = null; private string _title = "Artist.Name-Album.Name-2018-Flac-Vinyl-Readarr"; [SetUp] public void Setup() { - _artist = Builder.CreateNew() + _artist = Builder.CreateNew() .With(s => s.Tags = new HashSet(new[] { 1, 2 })) .Build(); diff --git a/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs b/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs index 09959a42d..6a18545f5 100644 --- a/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs +++ b/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs @@ -24,9 +24,9 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests { @"30 Rock1.mp3", @"30 Rock2.flac", - @"30 Rock3.ogg", - @"30 Rock4.m4a", - @"30 Rock.avi", + @"30 Rock3.pdf", + @"30 Rock4.epub", + @"30 Rock.mobi", @"movie.exe", @"movie" }; @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests { GivenFiles(GetFiles(_path)); - Subject.GetAudioFiles(_path).Should().HaveCount(4); + Subject.GetAudioFiles(_path).Should().HaveCount(3); } [TestCase("Extras")] diff --git a/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs index f7a659578..791ebd053 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs @@ -26,7 +26,7 @@ namespace NzbDrone.Core.Test.Qualities .Setup(s => s.All()) .Returns(new List { - new QualityDefinition(Quality.MP3_192) { Weight = 1, MinSize = 0, MaxSize = 100, Id = 20 } + new QualityDefinition(Quality.MP3_320) { Weight = 1, MinSize = 0, MaxSize = 100, Id = 20 } }); Subject.Handle(new ApplicationStartedEvent()); @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.Qualities .Setup(s => s.All()) .Returns(new List { - new QualityDefinition(Quality.MP3_192) { Weight = 1, MinSize = 0, MaxSize = 100, Id = 20 } + new QualityDefinition(Quality.MP3_320) { Weight = 1, MinSize = 0, MaxSize = 100, Id = 20 } }); Subject.Handle(new ApplicationStartedEvent()); diff --git a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs index 3a72522c8..e858e3bf9 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs @@ -14,21 +14,23 @@ namespace NzbDrone.Core.Test.Qualities public static object[] FromIntCases = { new object[] { 0, Quality.Unknown }, - new object[] { 1, Quality.MP3_192 }, - new object[] { 2, Quality.MP3_VBR }, - new object[] { 3, Quality.MP3_256 }, - new object[] { 4, Quality.MP3_320 }, - new object[] { 6, Quality.FLAC }, + new object[] { 1, Quality.PDF }, + new object[] { 2, Quality.MOBI }, + new object[] { 3, Quality.EPUB }, + new object[] { 4, Quality.AZW3 }, + new object[] { 10, Quality.MP3_320 }, + new object[] { 11, Quality.FLAC }, }; public static object[] ToIntCases = { new object[] { Quality.Unknown, 0 }, - new object[] { Quality.MP3_192, 1 }, - new object[] { Quality.MP3_VBR, 2 }, - new object[] { Quality.MP3_256, 3 }, - new object[] { Quality.MP3_320, 4 }, - new object[] { Quality.FLAC, 6 }, + new object[] { Quality.PDF, 1 }, + new object[] { Quality.MOBI, 2 }, + new object[] { Quality.EPUB, 3 }, + new object[] { Quality.AZW3, 4 }, + new object[] { Quality.MP3_320, 10 }, + new object[] { Quality.FLAC, 11 }, }; [Test] @@ -52,11 +54,11 @@ namespace NzbDrone.Core.Test.Qualities var qualities = new List { Quality.Unknown, - Quality.MP3_192, - Quality.MP3_VBR, - Quality.MP3_256, + Quality.MOBI, + Quality.EPUB, + Quality.AZW3, Quality.MP3_320, - Quality.FLAC, + Quality.FLAC }; if (allowed.Length == 0) diff --git a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs index 67c57f5ae..cd124cf74 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.Qualities private void GivenCustomProfile() { - Subject = new QualityModelComparer(new QualityProfile { Items = QualityFixture.GetDefaultQualities(Quality.MP3_320, Quality.MP3_192) }); + Subject = new QualityModelComparer(new QualityProfile { Items = QualityFixture.GetDefaultQualities(Quality.AZW3, Quality.MOBI) }); } private void GivenGroupedProfile() @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.Qualities new QualityProfileQualityItem { Allowed = false, - Quality = Quality.MP3_192 + Quality = Quality.MOBI }, new QualityProfileQualityItem { @@ -41,12 +41,12 @@ namespace NzbDrone.Core.Test.Qualities new QualityProfileQualityItem { Allowed = true, - Quality = Quality.MP3_256 + Quality = Quality.EPUB }, new QualityProfileQualityItem { Allowed = true, - Quality = Quality.MP3_320 + Quality = Quality.AZW3 } } }, @@ -66,8 +66,8 @@ namespace NzbDrone.Core.Test.Qualities { GivenDefaultProfile(); - var first = new QualityModel(Quality.MP3_320); - var second = new QualityModel(Quality.MP3_192); + var first = new QualityModel(Quality.FLAC); + var second = new QualityModel(Quality.MOBI); var compare = Subject.Compare(first, second); @@ -79,8 +79,8 @@ namespace NzbDrone.Core.Test.Qualities { GivenDefaultProfile(); - var first = new QualityModel(Quality.MP3_192); - var second = new QualityModel(Quality.MP3_320); + var first = new QualityModel(Quality.MOBI); + var second = new QualityModel(Quality.FLAC); var compare = Subject.Compare(first, second); @@ -92,8 +92,8 @@ namespace NzbDrone.Core.Test.Qualities { GivenDefaultProfile(); - var first = new QualityModel(Quality.MP3_320, new Revision(version: 2)); - var second = new QualityModel(Quality.MP3_320, new Revision(version: 1)); + var first = new QualityModel(Quality.MOBI, new Revision(version: 2)); + var second = new QualityModel(Quality.MOBI, new Revision(version: 1)); var compare = Subject.Compare(first, second); @@ -105,8 +105,8 @@ namespace NzbDrone.Core.Test.Qualities { GivenCustomProfile(); - var first = new QualityModel(Quality.MP3_192); - var second = new QualityModel(Quality.MP3_320); + var first = new QualityModel(Quality.MOBI); + var second = new QualityModel(Quality.AZW3); var compare = Subject.Compare(first, second); @@ -118,8 +118,8 @@ namespace NzbDrone.Core.Test.Qualities { GivenGroupedProfile(); - var first = new QualityModel(Quality.MP3_256); - var second = new QualityModel(Quality.MP3_320); + var first = new QualityModel(Quality.EPUB); + var second = new QualityModel(Quality.AZW3); var compare = Subject.Compare(first, second); @@ -131,8 +131,8 @@ namespace NzbDrone.Core.Test.Qualities { GivenGroupedProfile(); - var first = new QualityModel(Quality.MP3_256); - var second = new QualityModel(Quality.MP3_320); + var first = new QualityModel(Quality.EPUB); + var second = new QualityModel(Quality.AZW3); var compare = Subject.Compare(first, second, true); diff --git a/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs b/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs index 7cb205af4..4f7ae4df0 100644 --- a/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs +++ b/src/NzbDrone.Core.Test/QueueTests/QueueServiceFixture.cs @@ -26,17 +26,17 @@ namespace NzbDrone.Core.Test.QueueTests .With(v => v.RemainingTime = TimeSpan.FromSeconds(10)) .Build(); - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .Build(); - var albums = Builder.CreateListOfSize(3) + var albums = Builder.CreateListOfSize(3) .All() - .With(e => e.ArtistId = artist.Id) + .With(e => e.AuthorId = artist.Id) .Build(); var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = artist) - .With(r => r.Albums = new List(albums)) + .With(r => r.Albums = new List(albums)) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo()) .Build(); diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs index d5c03431e..7b1007a49 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.UpdateTests } [Test] - [Platform(Exclude = "NetCore")] + [Ignore("Ignore until we actually release something on nightly")] public void finds_update_when_version_lower() { UseRealHttp(); @@ -41,6 +41,7 @@ namespace NzbDrone.Core.Test.UpdateTests } [Test] + [Ignore("Until merge readarr 0.1 pr")] public void should_get_recent_updates() { const string branch = "nightly"; diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs index 0c4eeee30..ab04aea3b 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs @@ -124,7 +124,7 @@ namespace NzbDrone.Core.Test.UpdateTests Subject.Execute(new ApplicationUpdateCommand()); - Mocker.GetMock().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive)); + Mocker.GetMock().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive, null)); } [Test] @@ -238,6 +238,7 @@ namespace NzbDrone.Core.Test.UpdateTests [Test] [IntegrationTest] + [Ignore("Until release published")] public void Should_download_and_extract_to_temp_folder() { UseRealHttp(); @@ -289,7 +290,7 @@ namespace NzbDrone.Core.Test.UpdateTests Assert.Throws(() => Subject.Execute(new ApplicationUpdateCommand())); - Mocker.GetMock().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive, null), Times.Never()); ExceptionVerification.ExpectedErrors(1); } @@ -304,7 +305,7 @@ namespace NzbDrone.Core.Test.UpdateTests Assert.Throws(() => Subject.Execute(new ApplicationUpdateCommand())); - Mocker.GetMock().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never()); + Mocker.GetMock().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive, null), Times.Never()); ExceptionVerification.ExpectedErrors(1); } diff --git a/src/NzbDrone.Core.Test/ValidationTests/SystemFolderValidatorFixture.cs b/src/NzbDrone.Core.Test/ValidationTests/SystemFolderValidatorFixture.cs index 025771e42..168cae893 100644 --- a/src/NzbDrone.Core.Test/ValidationTests/SystemFolderValidatorFixture.cs +++ b/src/NzbDrone.Core.Test/ValidationTests/SystemFolderValidatorFixture.cs @@ -13,12 +13,12 @@ namespace NzbDrone.Core.Test.ValidationTests { public class SystemFolderValidatorFixture : CoreTest { - private TestValidator _validator; + private TestValidator _validator; [SetUp] public void Setup() { - _validator = new TestValidator + _validator = new TestValidator { v => v.RuleFor(s => s.Path).SetValidator(Subject) }; @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.ValidationTests { WindowsOnly(); - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .With(s => s.Path = Environment.GetFolderPath(Environment.SpecialFolder.Windows)) .Build(); @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.ValidationTests { WindowsOnly(); - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .With(s => s.Path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Test")) .Build(); @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Test.ValidationTests PosixOnly(); var bin = OsInfo.IsOsx ? "/System" : "/bin"; - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .With(s => s.Path = bin) .Build(); @@ -67,7 +67,7 @@ namespace NzbDrone.Core.Test.ValidationTests PosixOnly(); var bin = OsInfo.IsOsx ? "/System" : "/bin"; - var artist = Builder.CreateNew() + var artist = Builder.CreateNew() .With(s => s.Path = Path.Combine(bin, "test")) .Build(); diff --git a/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs b/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs index ffc84ad82..3c81e1297 100644 --- a/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs +++ b/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs @@ -4,8 +4,8 @@ namespace NzbDrone.Core.ArtistStats { public class AlbumStatistics : ResultSet { - public int ArtistId { get; set; } - public int AlbumId { get; set; } + public int AuthorId { get; set; } + public int BookId { get; set; } public int TrackFileCount { get; set; } public int TrackCount { get; set; } public int AvailableTrackCount { get; set; } diff --git a/src/NzbDrone.Core/ArtistStats/ArtistStatistics.cs b/src/NzbDrone.Core/ArtistStats/ArtistStatistics.cs index 9315eb4c5..71a5394f9 100644 --- a/src/NzbDrone.Core/ArtistStats/ArtistStatistics.cs +++ b/src/NzbDrone.Core/ArtistStats/ArtistStatistics.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Core.ArtistStats { public class ArtistStatistics : ResultSet { - public int ArtistId { get; set; } + public int AuthorId { get; set; } public int AlbumCount { get; set; } public int TrackFileCount { get; set; } public int TrackCount { get; set; } diff --git a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs index 29ec139ba..e5590da56 100644 --- a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs +++ b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs @@ -11,12 +11,12 @@ namespace NzbDrone.Core.ArtistStats public interface IArtistStatisticsRepository { List ArtistStatistics(); - List ArtistStatistics(int artistId); + List ArtistStatistics(int authorId); } public class ArtistStatisticsRepository : IArtistStatisticsRepository { - private const string _selectTemplate = "SELECT /**select**/ FROM Tracks /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; + private const string _selectTemplate = "SELECT /**select**/ FROM Books /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; private readonly IMainDatabase _database; @@ -28,14 +28,14 @@ namespace NzbDrone.Core.ArtistStats public List ArtistStatistics() { var time = DateTime.UtcNow; - return Query(Builder().Where(x => x.ReleaseDate < time)); + return Query(Builder().Where(x => x.ReleaseDate < time)); } - public List ArtistStatistics(int artistId) + public List ArtistStatistics(int authorId) { var time = DateTime.UtcNow; - return Query(Builder().Where(x => x.ReleaseDate < time) - .Where(x => x.Id == artistId)); + return Query(Builder().Where(x => x.ReleaseDate < time) + .Where(x => x.Id == authorId)); } private List Query(SqlBuilder builder) @@ -49,19 +49,16 @@ namespace NzbDrone.Core.ArtistStats } private SqlBuilder Builder() => new SqlBuilder() - .Select(@"Artists.Id AS ArtistId, - Albums.Id AS AlbumId, - SUM(COALESCE(TrackFiles.Size, 0)) AS SizeOnDisk, - COUNT(Tracks.Id) AS TotalTrackCount, - SUM(CASE WHEN Tracks.TrackFileId > 0 THEN 1 ELSE 0 END) AS AvailableTrackCount, - SUM(CASE WHEN Albums.Monitored = 1 OR Tracks.TrackFileId > 0 THEN 1 ELSE 0 END) AS TrackCount, - SUM(CASE WHEN TrackFiles.Id IS NULL THEN 0 ELSE 1 END) AS TrackFileCount") - .Join((t, r) => t.AlbumReleaseId == r.Id) - .Join((r, a) => r.AlbumId == a.Id) - .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) - .LeftJoin((t, f) => t.TrackFileId == f.Id) - .Where(x => x.Monitored == true) - .GroupBy(x => x.Id) - .GroupBy(x => x.Id); + .Select(@"Authors.Id AS AuthorId, + Books.Id AS BookId, + SUM(COALESCE(BookFiles.Size, 0)) AS SizeOnDisk, + COUNT(Books.Id) AS TotalTrackCount, + SUM(CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END) AS AvailableTrackCount, + SUM(CASE WHEN Books.Monitored = 1 OR BookFiles.Id IS NOT NULL THEN 1 ELSE 0 END) AS TrackCount, + SUM(CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END) AS TrackFileCount") + .Join((album, artist) => album.AuthorMetadataId == artist.AuthorMetadataId) + .LeftJoin((t, f) => t.Id == f.BookId) + .GroupBy(x => x.Id) + .GroupBy(x => x.Id); } } diff --git a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsService.cs b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsService.cs index 5810387a7..ebfe6e60d 100644 --- a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsService.cs +++ b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsService.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Core.ArtistStats public interface IArtistStatisticsService { List ArtistStatistics(); - ArtistStatistics ArtistStatistics(int artistId); + ArtistStatistics ArtistStatistics(int authorId); } public class ArtistStatisticsService : IArtistStatisticsService, @@ -37,12 +37,12 @@ namespace NzbDrone.Core.ArtistStats { var albumStatistics = _cache.Get("AllArtists", () => _artistStatisticsRepository.ArtistStatistics()); - return albumStatistics.GroupBy(s => s.ArtistId).Select(s => MapArtistStatistics(s.ToList())).ToList(); + return albumStatistics.GroupBy(s => s.AuthorId).Select(s => MapArtistStatistics(s.ToList())).ToList(); } - public ArtistStatistics ArtistStatistics(int artistId) + public ArtistStatistics ArtistStatistics(int authorId) { - var stats = _cache.Get(artistId.ToString(), () => _artistStatisticsRepository.ArtistStatistics(artistId)); + var stats = _cache.Get(authorId.ToString(), () => _artistStatisticsRepository.ArtistStatistics(authorId)); if (stats == null || stats.Count == 0) { @@ -58,7 +58,7 @@ namespace NzbDrone.Core.ArtistStats { AlbumStatistics = albumStatistics, AlbumCount = albumStatistics.Count, - ArtistId = albumStatistics.First().ArtistId, + AuthorId = albumStatistics.First().AuthorId, TrackFileCount = albumStatistics.Sum(s => s.TrackFileCount), TrackCount = albumStatistics.Sum(s => s.TrackCount), TotalTrackCount = albumStatistics.Sum(s => s.TotalTrackCount), @@ -86,14 +86,14 @@ namespace NzbDrone.Core.ArtistStats public void Handle(AlbumAddedEvent message) { _cache.Remove("AllArtists"); - _cache.Remove(message.Album.ArtistId.ToString()); + _cache.Remove(message.Album.AuthorId.ToString()); } [EventHandleOrder(EventHandleOrder.First)] public void Handle(AlbumDeletedEvent message) { _cache.Remove("AllArtists"); - _cache.Remove(message.Album.ArtistId.ToString()); + _cache.Remove(message.Album.AuthorId.ToString()); } [EventHandleOrder(EventHandleOrder.First)] @@ -107,7 +107,7 @@ namespace NzbDrone.Core.ArtistStats public void Handle(AlbumEditedEvent message) { _cache.Remove("AllArtists"); - _cache.Remove(message.Album.ArtistId.ToString()); + _cache.Remove(message.Album.AuthorId.ToString()); } [EventHandleOrder(EventHandleOrder.First)] diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs index 301220de9..1cd7a5199 100644 --- a/src/NzbDrone.Core/Blacklisting/Blacklist.cs +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -9,9 +9,9 @@ namespace NzbDrone.Core.Blacklisting { public class Blacklist : ModelBase { - public int ArtistId { get; set; } - public Artist Artist { get; set; } - public List AlbumIds { get; set; } + public int AuthorId { get; set; } + public Author Artist { get; set; } + public List BookIds { get; set; } public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs index 63cb0eaf2..b5e452c21 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs @@ -8,9 +8,9 @@ namespace NzbDrone.Core.Blacklisting { public interface IBlacklistRepository : IBasicRepository { - List BlacklistedByTitle(int artistId, string sourceTitle); - List BlacklistedByTorrentInfoHash(int artistId, string torrentInfoHash); - List BlacklistedByArtist(int artistId); + List BlacklistedByTitle(int authorId, string sourceTitle); + List BlacklistedByTorrentInfoHash(int authorId, string torrentInfoHash); + List BlacklistedByArtist(int authorId); } public class BlacklistRepository : BasicRepository, IBlacklistRepository @@ -20,23 +20,23 @@ namespace NzbDrone.Core.Blacklisting { } - public List BlacklistedByTitle(int artistId, string sourceTitle) + public List BlacklistedByTitle(int authorId, string sourceTitle) { - return Query(e => e.ArtistId == artistId && e.SourceTitle.Contains(sourceTitle)); + return Query(e => e.AuthorId == authorId && e.SourceTitle.Contains(sourceTitle)); } - public List BlacklistedByTorrentInfoHash(int artistId, string torrentInfoHash) + public List BlacklistedByTorrentInfoHash(int authorId, string torrentInfoHash) { - return Query(e => e.ArtistId == artistId && e.TorrentInfoHash.Contains(torrentInfoHash)); + return Query(e => e.AuthorId == authorId && e.TorrentInfoHash.Contains(torrentInfoHash)); } - public List BlacklistedByArtist(int artistId) + public List BlacklistedByArtist(int authorId) { - return Query(b => b.ArtistId == artistId); + return Query(b => b.AuthorId == authorId); } - protected override SqlBuilder PagedBuilder() => new SqlBuilder().Join((b, m) => b.ArtistId == m.Id); - protected override IEnumerable PagedQuery(SqlBuilder builder) => _database.QueryJoined(builder, (bl, artist) => + protected override SqlBuilder PagedBuilder() => new SqlBuilder().Join((b, m) => b.AuthorId == m.Id); + protected override IEnumerable PagedQuery(SqlBuilder builder) => _database.QueryJoined(builder, (bl, artist) => { bl.Artist = artist; return bl; diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs index 2ce1f9d7d..c9a363287 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Blacklisting { public interface IBlacklistService { - bool Blacklisted(int artistId, ReleaseInfo release); + bool Blacklisted(int authorId, ReleaseInfo release); PagingSpec Paged(PagingSpec pagingSpec); void Delete(int id); } @@ -32,9 +32,9 @@ namespace NzbDrone.Core.Blacklisting _blacklistRepository = blacklistRepository; } - public bool Blacklisted(int artistId, ReleaseInfo release) + public bool Blacklisted(int authorId, ReleaseInfo release) { - var blacklistedByTitle = _blacklistRepository.BlacklistedByTitle(artistId, release.Title); + var blacklistedByTitle = _blacklistRepository.BlacklistedByTitle(authorId, release.Title); if (release.DownloadProtocol == DownloadProtocol.Torrent) { @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Blacklisting .Any(b => SameTorrent(b, torrentInfo)); } - var blacklistedByTorrentInfohash = _blacklistRepository.BlacklistedByTorrentInfoHash(artistId, torrentInfo.InfoHash); + var blacklistedByTorrentInfohash = _blacklistRepository.BlacklistedByTorrentInfoHash(authorId, torrentInfo.InfoHash); return blacklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo)); } @@ -139,8 +139,8 @@ namespace NzbDrone.Core.Blacklisting { var blacklist = new Blacklist { - ArtistId = message.ArtistId, - AlbumIds = message.AlbumIds, + AuthorId = message.AuthorId, + BookIds = message.BookIds, SourceTitle = message.SourceTitle, Quality = message.Quality, Date = DateTime.UtcNow, diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreBook.cs b/src/NzbDrone.Core/Books/Calibre/CalibreBook.cs new file mode 100644 index 000000000..042f73cf4 --- /dev/null +++ b/src/NzbDrone.Core/Books/Calibre/CalibreBook.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Books.Calibre +{ + public class CalibreBook + { + [JsonProperty("format_metadata")] + public Dictionary Formats { get; set; } + + [JsonProperty("author_sort")] + public string AuthorSort { get; set; } + + public string Title { get; set; } + + public string Series { get; set; } + + [JsonProperty("series_index")] + public string Position { get; set; } + + public Dictionary Identifiers { get; set; } + } + + public class CalibreBookFormat + { + public string Path { get; set; } + + public long Size { get; set; } + + [JsonProperty("mtime")] + public DateTime LastModified { get; set; } + } +} diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreConversionOptions.cs b/src/NzbDrone.Core/Books/Calibre/CalibreConversionOptions.cs new file mode 100644 index 000000000..acae49375 --- /dev/null +++ b/src/NzbDrone.Core/Books/Calibre/CalibreConversionOptions.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Books.Calibre +{ + public enum CalibreFormat + { + None, + EPUB, + AZW3, + MOBI, + DOCX, + FB2, + HTMLZ, + LIT, + LRF, + PDB, + PDF, + PMLZ, + RB, + RTF, + SNB, + TCR, + TXT, + TXTZ, + ZIP + } + + public enum CalibreProfile + { + Default, + cybookg3, + cybook_opus, + generic_eink, + generic_eink_hd, + generic_eink_large, + hanlinv3, + hanlinv5, + illiad, + ipad, + ipad3, + irexdr1000, + irexdr800, + jetbook5, + kindle, + kindle_dx, + kindle_fire, + kindle_oasis, + kindle_pw, + kindle_pw3, + kindle_voyage, + kobo, + msreader, + mobipocket, + nook, + nook_color, + nook_hd_plus, + pocketbook_900, + pocketbook_pro_912, + galaxy, + sony, + sony300, + sony900, + sony_landscape, + sonyt3, + tablet + } + + public class CalibreBookData + { + public CalibreConversionOptions Conversion_options { get; set; } + public int Book_id { get; set; } + public List Input_formats { get; set; } + public List Output_formats { get; set; } + } + + public class CalibreConversionOptions + { + public CalibreOptions Options { get; set; } + public string Input_fmt { get; set; } + public string Output_fmt { get; set; } + } + + public class CalibreOptions + { + public string Output_profile { get; set; } + } +} diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreConversionStatus.cs b/src/NzbDrone.Core/Books/Calibre/CalibreConversionStatus.cs new file mode 100644 index 000000000..8230db84d --- /dev/null +++ b/src/NzbDrone.Core/Books/Calibre/CalibreConversionStatus.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Books.Calibre +{ + public class CalibreConversionStatus + { + public bool Running { get; set; } + + public bool Ok { get; set; } + + [JsonProperty("was_aborted")] + public bool WasAborted { get; set; } + + public string Traceback { get; set; } + + public string Log { get; set; } + } +} diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreException.cs b/src/NzbDrone.Core/Books/Calibre/CalibreException.cs new file mode 100644 index 000000000..ed985bafb --- /dev/null +++ b/src/NzbDrone.Core/Books/Calibre/CalibreException.cs @@ -0,0 +1,18 @@ +using System; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Books.Calibre +{ + public class CalibreException : NzbDroneException + { + public CalibreException(string message) + : base(message) + { + } + + public CalibreException(string message, Exception innerException, params object[] args) + : base(message, innerException, args) + { + } + } +} diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreImportJob.cs b/src/NzbDrone.Core/Books/Calibre/CalibreImportJob.cs new file mode 100644 index 000000000..e65ad7c1a --- /dev/null +++ b/src/NzbDrone.Core/Books/Calibre/CalibreImportJob.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Books.Calibre +{ + public class CalibreImportJob + { + [JsonProperty("book_id")] + public int Id { get; set; } + [JsonProperty("id")] + public int JobId { get; set; } + public string Filename { get; set; } + public List Authors { get; set; } + public string Title { get; set; } + public List Languages { get; set; } + } +} diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreProxy.cs b/src/NzbDrone.Core/Books/Calibre/CalibreProxy.cs new file mode 100644 index 000000000..8688bbdcc --- /dev/null +++ b/src/NzbDrone.Core/Books/Calibre/CalibreProxy.cs @@ -0,0 +1,320 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Rest; + +namespace NzbDrone.Core.Books.Calibre +{ + public interface ICalibreProxy + { + void GetLibraryInfo(CalibreSettings settings); + CalibreImportJob AddBook(BookFile book, CalibreSettings settings); + void AddFormat(BookFile file, CalibreSettings settings); + void RemoveFormats(int calibreId, IEnumerable formats, CalibreSettings settings); + void SetFields(BookFile file, CalibreSettings settings); + CalibreBookData GetBookData(int calibreId, CalibreSettings settings); + long ConvertBook(int calibreId, CalibreConversionOptions options, CalibreSettings settings); + List GetAllBookFilePaths(CalibreSettings settings); + CalibreBook GetBook(int calibreId, CalibreSettings settings); + } + + public class CalibreProxy : ICalibreProxy + { + private readonly IHttpClient _httpClient; + private readonly IMapCoversToLocal _mediaCoverService; + private readonly IRemotePathMappingService _pathMapper; + private readonly Logger _logger; + private readonly ICached _bookCache; + + public CalibreProxy(IHttpClient httpClient, + IMapCoversToLocal mediaCoverService, + IRemotePathMappingService pathMapper, + ICacheManager cacheManager, + Logger logger) + { + _httpClient = httpClient; + _mediaCoverService = mediaCoverService; + _pathMapper = pathMapper; + _bookCache = cacheManager.GetCache(GetType()); + _logger = logger; + } + + public CalibreImportJob AddBook(BookFile book, CalibreSettings settings) + { + var jobid = (int)(DateTime.UtcNow.Ticks % 1000000000); + var addDuplicates = false; + var path = book.Path; + var filename = $"$dummy{Path.GetExtension(path)}"; + var body = File.ReadAllBytes(path); + + _logger.Trace($"Read {body.Length} bytes from {path}"); + + try + { + var builder = GetBuilder($"cdb/add-book/{jobid}/{addDuplicates}/{filename}", settings); + + var request = builder.Build(); + request.SetContent(body); + + return _httpClient.Post(request).Resource; + } + catch (RestException ex) + { + throw new CalibreException("Unable to add file to calibre library: {0}", ex, ex.Message); + } + } + + public void AddFormat(BookFile file, CalibreSettings settings) + { + var format = Path.GetExtension(file.Path); + var bookData = Convert.ToBase64String(File.ReadAllBytes(file.Path)); + + var payload = new + { + changes = new + { + added_formats = new[] + { + new + { + ext = format, + data_url = bookData + } + } + }, + loaded_book_ids = new[] { file.CalibreId } + }; + + ExecuteSetFields(file.CalibreId, payload, settings); + } + + public void RemoveFormats(int calibreId, IEnumerable formats, CalibreSettings settings) + { + var payload = new + { + changes = new + { + removed_formats = formats + }, + + loaded_book_ids = new[] { calibreId } + }; + + ExecuteSetFields(calibreId, payload, settings); + } + + public void SetFields(BookFile file, CalibreSettings settings) + { + var book = file.Album.Value; + + var cover = book.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover); + string image = null; + if (cover != null) + { + var imageFile = _mediaCoverService.GetCoverPath(book.Id, MediaCoverEntity.Album, cover.CoverType, cover.Extension, null); + + if (File.Exists(imageFile)) + { + var imageData = File.ReadAllBytes(imageFile); + image = Convert.ToBase64String(imageData); + } + } + + var payload = new + { + changes = new + { + title = book.Title, + authors = new[] { file.Artist.Value.Name }, + cover = image, + pubdate = book.ReleaseDate, + comments = book.Overview, + rating = book.Ratings.Value * 2, + identifiers = new Dictionary + { + { "goodreads", book.GoodreadsId.ToString() }, + { "isbn", book.Isbn13 }, + { "asin", book.Asin } + } + }, + loaded_book_ids = new[] { file.CalibreId } + }; + + ExecuteSetFields(file.CalibreId, payload, settings); + } + + private void ExecuteSetFields(int id, object payload, CalibreSettings settings) + { + var builder = GetBuilder($"cdb/set-fields/{id}", settings) + .Post() + .SetHeader("Content-Type", "application/json"); + + var request = builder.Build(); + request.SetContent(payload.ToJson()); + + _httpClient.Execute(request); + } + + public CalibreBookData GetBookData(int calibreId, CalibreSettings settings) + { + try + { + var builder = GetBuilder($"conversion/book-data/{calibreId}", settings); + + var request = builder.Build(); + + return _httpClient.Get(request).Resource; + } + catch (RestException ex) + { + throw new CalibreException("Unable to add file to calibre library: {0}", ex, ex.Message); + } + } + + public long ConvertBook(int calibreId, CalibreConversionOptions options, CalibreSettings settings) + { + try + { + var builder = GetBuilder($"conversion/start/{calibreId}", settings); + + var request = builder.Build(); + request.SetContent(options.ToJson()); + + var jobId = _httpClient.Post(request).Resource; + + // Run async task to check if conversion complete + _ = PollConvertStatus(jobId, settings); + + return jobId; + } + catch (RestException ex) + { + throw new CalibreException("Unable to start calibre conversion: {0}", ex, ex.Message); + } + } + + public CalibreBook GetBook(int calibreId, CalibreSettings settings) + { + try + { + var builder = GetBuilder($"ajax/book/{calibreId}", settings); + + var request = builder.Build(); + var book = _httpClient.Get(request).Resource; + + foreach (var format in book.Formats.Values) + { + format.Path = _pathMapper.RemapRemoteToLocal(settings.Host, new OsPath(format.Path)).FullPath; + } + + return book; + } + catch (RestException ex) + { + throw new CalibreException("Unable to connect to calibre library: {0}", ex, ex.Message); + } + } + + public List GetAllBookFilePaths(CalibreSettings settings) + { + _bookCache.Clear(); + + try + { + var builder = GetBuilder($"ajax/books", settings); + + var request = builder.Build(); + var response = _httpClient.Get>(request); + + var result = new List(); + + foreach (var book in response.Resource.Values) + { + var remotePath = book?.Formats.Values.OrderBy(f => f.LastModified).FirstOrDefault()?.Path; + if (remotePath == null) + { + continue; + } + + var localPath = _pathMapper.RemapRemoteToLocal(settings.Host, new OsPath(remotePath)).FullPath; + result.Add(localPath); + + _bookCache.Set(localPath, book, TimeSpan.FromMinutes(5)); + } + + return result; + } + catch (RestException ex) + { + throw new CalibreException("Unable to connect to calibre library: {0}", ex, ex.Message); + } + } + + public void GetLibraryInfo(CalibreSettings settings) + { + try + { + var builder = GetBuilder($"ajax/library-info", settings); + var request = builder.Build(); + var response = _httpClient.Execute(request); + } + catch (RestException ex) + { + throw new CalibreException("Unable to connect to calibre library: {0}", ex, ex.Message); + } + } + + private HttpRequestBuilder GetBuilder(string relativePath, CalibreSettings settings) + { + var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase); + baseUrl = HttpUri.CombinePath(baseUrl, relativePath); + + var builder = new HttpRequestBuilder(baseUrl) + .Accept(HttpAccept.Json); + + builder.LogResponseContent = true; + + if (settings.Username.IsNotNullOrWhiteSpace()) + { + builder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); + } + + return builder; + } + + private async Task PollConvertStatus(long jobId, CalibreSettings settings) + { + var builder = GetBuilder($"/conversion/status/{jobId}", settings); + var request = builder.Build(); + + while (true) + { + var status = _httpClient.Get(request).Resource; + + if (!status.Running) + { + if (!status.Ok) + { + _logger.Warn("Calibre conversion failed.\n{0}\n{1}", status.Traceback, status.Log); + } + + return; + } + + await Task.Delay(2000); + } + } + } +} diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs b/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs new file mode 100644 index 000000000..f90e8006d --- /dev/null +++ b/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Books.Calibre +{ + public class CalibreSettingsValidator : AbstractValidator + { + public CalibreSettingsValidator() + { + RuleFor(c => c.Host).IsValidUrl(); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); + RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace()); + + RuleFor(c => c.Username).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Password)); + RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username)); + + RuleFor(c => c.OutputFormat).Must(x => x.Split(',').All(y => Enum.TryParse(y, true, out _))).WithMessage("Invalid output formats"); + } + } + + public class CalibreSettings : IProviderConfig + { + private static readonly CalibreSettingsValidator Validator = new CalibreSettingsValidator(); + + public CalibreSettings() + { + Port = 8080; + } + + [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] + public string Host { get; set; } + + [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] + public int Port { get; set; } + + [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the calibre url, e.g. http://[host]:[port]/[urlBase]")] + public string UrlBase { get; set; } + + [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)] + public string Username { get; set; } + + [FieldDefinition(4, Label = "Password", Type = FieldType.Password)] + public string Password { get; set; } + + [FieldDefinition(5, Label = "Convert to Format", Type = FieldType.Textbox, HelpText = "Optionally ask calibre to convert to other formats on import. Comma separated list.")] + public string OutputFormat { get; set; } + + [FieldDefinition(6, Label = "Conversion Profile", Type = FieldType.Select, SelectOptions = typeof(CalibreProfile), HelpText = "The output profile to use for conversion")] + public int OutputProfile { get; set; } + + [FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)] + public bool UseSsl { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs b/src/NzbDrone.Core/Books/Commands/BulkMoveArtistCommand.cs similarity index 83% rename from src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs rename to src/NzbDrone.Core/Books/Commands/BulkMoveArtistCommand.cs index 8f035792b..7895a6f51 100644 --- a/src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs +++ b/src/NzbDrone.Core/Books/Commands/BulkMoveArtistCommand.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Music.Commands public class BulkMoveArtist : IEquatable { - public int ArtistId { get; set; } + public int AuthorId { get; set; } public string SourcePath { get; set; } public bool Equals(BulkMoveArtist other) @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Music.Commands return false; } - return ArtistId.Equals(other.ArtistId); + return AuthorId.Equals(other.AuthorId); } public override bool Equals(object obj) @@ -40,12 +40,12 @@ namespace NzbDrone.Core.Music.Commands return false; } - return ArtistId.Equals(((BulkMoveArtist)obj).ArtistId); + return AuthorId.Equals(((BulkMoveArtist)obj).AuthorId); } public override int GetHashCode() { - return ArtistId.GetHashCode(); + return AuthorId.GetHashCode(); } } } diff --git a/src/NzbDrone.Core/Music/Commands/BulkRefreshArtistCommand.cs b/src/NzbDrone.Core/Books/Commands/BulkRefreshArtistCommand.cs similarity index 77% rename from src/NzbDrone.Core/Music/Commands/BulkRefreshArtistCommand.cs rename to src/NzbDrone.Core/Books/Commands/BulkRefreshArtistCommand.cs index cba189a3b..63d077596 100644 --- a/src/NzbDrone.Core/Music/Commands/BulkRefreshArtistCommand.cs +++ b/src/NzbDrone.Core/Books/Commands/BulkRefreshArtistCommand.cs @@ -9,13 +9,13 @@ namespace NzbDrone.Core.Music.Commands { } - public BulkRefreshArtistCommand(List artistIds, bool areNewArtists = false) + public BulkRefreshArtistCommand(List authorIds, bool areNewArtists = false) { - ArtistIds = artistIds; + AuthorIds = authorIds; AreNewArtists = areNewArtists; } - public List ArtistIds { get; set; } + public List AuthorIds { get; set; } public bool AreNewArtists { get; set; } public override bool SendUpdatesToClient => true; diff --git a/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs b/src/NzbDrone.Core/Books/Commands/MoveArtistCommand.cs similarity index 89% rename from src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs rename to src/NzbDrone.Core/Books/Commands/MoveArtistCommand.cs index c120eddd4..7e7dc5f8a 100644 --- a/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs +++ b/src/NzbDrone.Core/Books/Commands/MoveArtistCommand.cs @@ -4,7 +4,7 @@ namespace NzbDrone.Core.Music.Commands { public class MoveArtistCommand : Command { - public int ArtistId { get; set; } + public int AuthorId { get; set; } public string SourcePath { get; set; } public string DestinationPath { get; set; } diff --git a/src/NzbDrone.Core/Music/Commands/RefreshAlbumCommand.cs b/src/NzbDrone.Core/Books/Commands/RefreshAlbumCommand.cs similarity index 59% rename from src/NzbDrone.Core/Music/Commands/RefreshAlbumCommand.cs rename to src/NzbDrone.Core/Books/Commands/RefreshAlbumCommand.cs index 40652db6e..9db176a0f 100644 --- a/src/NzbDrone.Core/Music/Commands/RefreshAlbumCommand.cs +++ b/src/NzbDrone.Core/Books/Commands/RefreshAlbumCommand.cs @@ -4,19 +4,19 @@ namespace NzbDrone.Core.Music.Commands { public class RefreshAlbumCommand : Command { - public int? AlbumId { get; set; } + public int? BookId { get; set; } public RefreshAlbumCommand() { } - public RefreshAlbumCommand(int? albumId) + public RefreshAlbumCommand(int? bookId) { - AlbumId = albumId; + BookId = bookId; } public override bool SendUpdatesToClient => true; - public override bool UpdateScheduledTask => !AlbumId.HasValue; + public override bool UpdateScheduledTask => !BookId.HasValue; } } diff --git a/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs b/src/NzbDrone.Core/Books/Commands/RefreshArtistCommand.cs similarity index 65% rename from src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs rename to src/NzbDrone.Core/Books/Commands/RefreshArtistCommand.cs index abd1dacd9..86825d49d 100644 --- a/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs +++ b/src/NzbDrone.Core/Books/Commands/RefreshArtistCommand.cs @@ -4,21 +4,21 @@ namespace NzbDrone.Core.Music.Commands { public class RefreshArtistCommand : Command { - public int? ArtistId { get; set; } + public int? AuthorId { get; set; } public bool IsNewArtist { get; set; } public RefreshArtistCommand() { } - public RefreshArtistCommand(int? artistId, bool isNewArtist = false) + public RefreshArtistCommand(int? authorId, bool isNewArtist = false) { - ArtistId = artistId; + AuthorId = authorId; IsNewArtist = isNewArtist; } public override bool SendUpdatesToClient => true; - public override bool UpdateScheduledTask => !ArtistId.HasValue; + public override bool UpdateScheduledTask => !AuthorId.HasValue; } } diff --git a/src/NzbDrone.Core/Books/Events/AlbumAddedEvent.cs b/src/NzbDrone.Core/Books/Events/AlbumAddedEvent.cs new file mode 100644 index 000000000..50ca77b43 --- /dev/null +++ b/src/NzbDrone.Core/Books/Events/AlbumAddedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Music.Events +{ + public class AlbumAddedEvent : IEvent + { + public Book Album { get; private set; } + public bool DoRefresh { get; private set; } + + public AlbumAddedEvent(Book album, bool doRefresh = true) + { + Album = album; + DoRefresh = doRefresh; + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/AlbumDeletedEvent.cs b/src/NzbDrone.Core/Books/Events/AlbumDeletedEvent.cs similarity index 73% rename from src/NzbDrone.Core/Music/Events/AlbumDeletedEvent.cs rename to src/NzbDrone.Core/Books/Events/AlbumDeletedEvent.cs index 23c08d8ed..4d7842a9a 100644 --- a/src/NzbDrone.Core/Music/Events/AlbumDeletedEvent.cs +++ b/src/NzbDrone.Core/Books/Events/AlbumDeletedEvent.cs @@ -4,11 +4,11 @@ namespace NzbDrone.Core.Music.Events { public class AlbumDeletedEvent : IEvent { - public Album Album { get; private set; } + public Book Album { get; private set; } public bool DeleteFiles { get; private set; } public bool AddImportListExclusion { get; private set; } - public AlbumDeletedEvent(Album album, bool deleteFiles, bool addImportListExclusion) + public AlbumDeletedEvent(Book album, bool deleteFiles, bool addImportListExclusion) { Album = album; DeleteFiles = deleteFiles; diff --git a/src/NzbDrone.Core/Music/Events/AlbumEditedEvent.cs b/src/NzbDrone.Core/Books/Events/AlbumEditedEvent.cs similarity index 56% rename from src/NzbDrone.Core/Music/Events/AlbumEditedEvent.cs rename to src/NzbDrone.Core/Books/Events/AlbumEditedEvent.cs index 7cd64cb19..8ef98bb15 100644 --- a/src/NzbDrone.Core/Music/Events/AlbumEditedEvent.cs +++ b/src/NzbDrone.Core/Books/Events/AlbumEditedEvent.cs @@ -4,10 +4,10 @@ namespace NzbDrone.Core.Music.Events { public class AlbumEditedEvent : IEvent { - public Album Album { get; private set; } - public Album OldAlbum { get; private set; } + public Book Album { get; private set; } + public Book OldAlbum { get; private set; } - public AlbumEditedEvent(Album album, Album oldAlbum) + public AlbumEditedEvent(Book album, Book oldAlbum) { Album = album; OldAlbum = oldAlbum; diff --git a/src/NzbDrone.Core/Books/Events/AlbumInfoRefreshedEvent.cs b/src/NzbDrone.Core/Books/Events/AlbumInfoRefreshedEvent.cs new file mode 100644 index 000000000..4e6ea3d76 --- /dev/null +++ b/src/NzbDrone.Core/Books/Events/AlbumInfoRefreshedEvent.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Music.Events +{ + public class AlbumInfoRefreshedEvent : IEvent + { + public Author Artist { get; set; } + public ReadOnlyCollection Added { get; private set; } + public ReadOnlyCollection Updated { get; private set; } + + public AlbumInfoRefreshedEvent(Author artist, IList added, IList updated) + { + Artist = artist; + Added = new ReadOnlyCollection(added); + Updated = new ReadOnlyCollection(updated); + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/AlbumUpdatedEvent.cs b/src/NzbDrone.Core/Books/Events/AlbumUpdatedEvent.cs similarity index 65% rename from src/NzbDrone.Core/Music/Events/AlbumUpdatedEvent.cs rename to src/NzbDrone.Core/Books/Events/AlbumUpdatedEvent.cs index 30fc7b86b..359ac3e9d 100644 --- a/src/NzbDrone.Core/Music/Events/AlbumUpdatedEvent.cs +++ b/src/NzbDrone.Core/Books/Events/AlbumUpdatedEvent.cs @@ -4,9 +4,9 @@ namespace NzbDrone.Core.Music.Events { public class AlbumUpdatedEvent : IEvent { - public Album Album { get; private set; } + public Book Album { get; private set; } - public AlbumUpdatedEvent(Album album) + public AlbumUpdatedEvent(Book album) { Album = album; } diff --git a/src/NzbDrone.Core/Music/Events/ArtistAddedEvent.cs b/src/NzbDrone.Core/Books/Events/ArtistAddedEvent.cs similarity index 70% rename from src/NzbDrone.Core/Music/Events/ArtistAddedEvent.cs rename to src/NzbDrone.Core/Books/Events/ArtistAddedEvent.cs index b4d827df1..e8e419d3d 100644 --- a/src/NzbDrone.Core/Music/Events/ArtistAddedEvent.cs +++ b/src/NzbDrone.Core/Books/Events/ArtistAddedEvent.cs @@ -4,10 +4,10 @@ namespace NzbDrone.Core.Music.Events { public class ArtistAddedEvent : IEvent { - public Artist Artist { get; private set; } + public Author Artist { get; private set; } public bool DoRefresh { get; private set; } - public ArtistAddedEvent(Artist artist, bool doRefresh = true) + public ArtistAddedEvent(Author artist, bool doRefresh = true) { Artist = artist; DoRefresh = doRefresh; diff --git a/src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs b/src/NzbDrone.Core/Books/Events/ArtistDeletedEvent.cs similarity index 79% rename from src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs rename to src/NzbDrone.Core/Books/Events/ArtistDeletedEvent.cs index 0c1bf0043..7cd120fc2 100644 --- a/src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs +++ b/src/NzbDrone.Core/Books/Events/ArtistDeletedEvent.cs @@ -4,11 +4,11 @@ namespace NzbDrone.Core.Music.Events { public class ArtistDeletedEvent : IEvent { - public Artist Artist { get; private set; } + public Author Artist { get; private set; } public bool DeleteFiles { get; private set; } public bool AddImportListExclusion { get; private set; } - public ArtistDeletedEvent(Artist artist, bool deleteFiles, bool addImportListExclusion) + public ArtistDeletedEvent(Author artist, bool deleteFiles, bool addImportListExclusion) { Artist = artist; DeleteFiles = deleteFiles; diff --git a/src/NzbDrone.Core/Music/Events/ArtistEditedEvent.cs b/src/NzbDrone.Core/Books/Events/ArtistEditedEvent.cs similarity index 56% rename from src/NzbDrone.Core/Music/Events/ArtistEditedEvent.cs rename to src/NzbDrone.Core/Books/Events/ArtistEditedEvent.cs index f1ca73b68..317cd7222 100644 --- a/src/NzbDrone.Core/Music/Events/ArtistEditedEvent.cs +++ b/src/NzbDrone.Core/Books/Events/ArtistEditedEvent.cs @@ -4,10 +4,10 @@ namespace NzbDrone.Core.Music.Events { public class ArtistEditedEvent : IEvent { - public Artist Artist { get; private set; } - public Artist OldArtist { get; private set; } + public Author Artist { get; private set; } + public Author OldArtist { get; private set; } - public ArtistEditedEvent(Artist artist, Artist oldArtist) + public ArtistEditedEvent(Author artist, Author oldArtist) { Artist = artist; OldArtist = oldArtist; diff --git a/src/NzbDrone.Core/Music/Events/ArtistMovedEvent.cs b/src/NzbDrone.Core/Books/Events/ArtistMovedEvent.cs similarity index 78% rename from src/NzbDrone.Core/Music/Events/ArtistMovedEvent.cs rename to src/NzbDrone.Core/Books/Events/ArtistMovedEvent.cs index 1de12f9f7..56bfa6270 100644 --- a/src/NzbDrone.Core/Music/Events/ArtistMovedEvent.cs +++ b/src/NzbDrone.Core/Books/Events/ArtistMovedEvent.cs @@ -4,11 +4,11 @@ namespace NzbDrone.Core.Music.Events { public class ArtistMovedEvent : IEvent { - public Artist Artist { get; set; } + public Author Artist { get; set; } public string SourcePath { get; set; } public string DestinationPath { get; set; } - public ArtistMovedEvent(Artist artist, string sourcePath, string destinationPath) + public ArtistMovedEvent(Author artist, string sourcePath, string destinationPath) { Artist = artist; SourcePath = sourcePath; diff --git a/src/NzbDrone.Core/Music/Events/ArtistRefreshCompleteEvent.cs b/src/NzbDrone.Core/Books/Events/ArtistRefreshCompleteEvent.cs similarity index 65% rename from src/NzbDrone.Core/Music/Events/ArtistRefreshCompleteEvent.cs rename to src/NzbDrone.Core/Books/Events/ArtistRefreshCompleteEvent.cs index 5214be4c3..1c0680a67 100644 --- a/src/NzbDrone.Core/Music/Events/ArtistRefreshCompleteEvent.cs +++ b/src/NzbDrone.Core/Books/Events/ArtistRefreshCompleteEvent.cs @@ -4,9 +4,9 @@ namespace NzbDrone.Core.Music.Events { public class ArtistRefreshCompleteEvent : IEvent { - public Artist Artist { get; set; } + public Author Artist { get; set; } - public ArtistRefreshCompleteEvent(Artist artist) + public ArtistRefreshCompleteEvent(Author artist) { Artist = artist; } diff --git a/src/NzbDrone.Core/Music/Events/ArtistUpdatedEvent.cs b/src/NzbDrone.Core/Books/Events/ArtistUpdatedEvent.cs similarity index 64% rename from src/NzbDrone.Core/Music/Events/ArtistUpdatedEvent.cs rename to src/NzbDrone.Core/Books/Events/ArtistUpdatedEvent.cs index 8555eba80..62e9c50c1 100644 --- a/src/NzbDrone.Core/Music/Events/ArtistUpdatedEvent.cs +++ b/src/NzbDrone.Core/Books/Events/ArtistUpdatedEvent.cs @@ -4,9 +4,9 @@ namespace NzbDrone.Core.Music.Events { public class ArtistUpdatedEvent : IEvent { - public Artist Artist { get; private set; } + public Author Artist { get; private set; } - public ArtistUpdatedEvent(Artist artist) + public ArtistUpdatedEvent(Author artist) { Artist = artist; } diff --git a/src/NzbDrone.Core/Books/Events/ArtistsImportedEvent.cs b/src/NzbDrone.Core/Books/Events/ArtistsImportedEvent.cs new file mode 100644 index 000000000..2a7fcbaf9 --- /dev/null +++ b/src/NzbDrone.Core/Books/Events/ArtistsImportedEvent.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Music.Events +{ + public class ArtistsImportedEvent : IEvent + { + public List AuthorIds { get; private set; } + public bool DoRefresh { get; private set; } + + public ArtistsImportedEvent(List authorIds, bool doRefresh = true) + { + AuthorIds = authorIds; + DoRefresh = doRefresh; + } + } +} diff --git a/src/NzbDrone.Core/Music/Handlers/AlbumAddedHandler.cs b/src/NzbDrone.Core/Books/Handlers/AlbumAddedHandler.cs similarity index 77% rename from src/NzbDrone.Core/Music/Handlers/AlbumAddedHandler.cs rename to src/NzbDrone.Core/Books/Handlers/AlbumAddedHandler.cs index e0c4da146..035f56dcf 100644 --- a/src/NzbDrone.Core/Music/Handlers/AlbumAddedHandler.cs +++ b/src/NzbDrone.Core/Books/Handlers/AlbumAddedHandler.cs @@ -16,7 +16,10 @@ namespace NzbDrone.Core.Music public void Handle(AlbumAddedEvent message) { - _commandQueueManager.Push(new RefreshArtistCommand(message.Album.Artist.Value.Id)); + if (message.DoRefresh) + { + _commandQueueManager.Push(new RefreshArtistCommand(message.Album.Author.Value.Id)); + } } } } diff --git a/src/NzbDrone.Core/Music/Handlers/ArtistAddedHandler.cs b/src/NzbDrone.Core/Books/Handlers/ArtistAddedHandler.cs similarity index 84% rename from src/NzbDrone.Core/Music/Handlers/ArtistAddedHandler.cs rename to src/NzbDrone.Core/Books/Handlers/ArtistAddedHandler.cs index a7cad3030..fc1c8a933 100644 --- a/src/NzbDrone.Core/Music/Handlers/ArtistAddedHandler.cs +++ b/src/NzbDrone.Core/Books/Handlers/ArtistAddedHandler.cs @@ -1,4 +1,3 @@ -using System.Linq; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Commands; @@ -26,7 +25,10 @@ namespace NzbDrone.Core.Music public void Handle(ArtistsImportedEvent message) { - _commandQueueManager.PushMany(message.ArtistIds.Select(s => new RefreshArtistCommand(s, true)).ToList()); + if (message.DoRefresh) + { + _commandQueueManager.Push(new BulkRefreshArtistCommand(message.AuthorIds, true)); + } } } } diff --git a/src/NzbDrone.Core/Music/Services/ArtistEditedService.cs b/src/NzbDrone.Core/Books/Handlers/ArtistEditedHandler.cs similarity index 100% rename from src/NzbDrone.Core/Music/Services/ArtistEditedService.cs rename to src/NzbDrone.Core/Books/Handlers/ArtistEditedHandler.cs diff --git a/src/NzbDrone.Core/Music/Handlers/ArtistScannedHandler.cs b/src/NzbDrone.Core/Books/Handlers/ArtistScannedHandler.cs similarity index 97% rename from src/NzbDrone.Core/Music/Handlers/ArtistScannedHandler.cs rename to src/NzbDrone.Core/Books/Handlers/ArtistScannedHandler.cs index 7a415e72b..5f514bac2 100644 --- a/src/NzbDrone.Core/Music/Handlers/ArtistScannedHandler.cs +++ b/src/NzbDrone.Core/Books/Handlers/ArtistScannedHandler.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Music _logger = logger; } - private void HandleScanEvents(Artist artist) + private void HandleScanEvents(Author artist) { if (artist.AddOptions != null) { diff --git a/src/NzbDrone.Core/Music/Model/AddAlbumOptions.cs b/src/NzbDrone.Core/Books/Model/AddAlbumOptions.cs similarity index 100% rename from src/NzbDrone.Core/Music/Model/AddAlbumOptions.cs rename to src/NzbDrone.Core/Books/Model/AddAlbumOptions.cs diff --git a/src/NzbDrone.Core/Music/Model/AddArtistOptions.cs b/src/NzbDrone.Core/Books/Model/AddArtistOptions.cs similarity index 100% rename from src/NzbDrone.Core/Music/Model/AddArtistOptions.cs rename to src/NzbDrone.Core/Books/Model/AddArtistOptions.cs diff --git a/src/NzbDrone.Core/Music/Model/ArtistStatusType.cs b/src/NzbDrone.Core/Books/Model/ArtistStatusType.cs similarity index 100% rename from src/NzbDrone.Core/Music/Model/ArtistStatusType.cs rename to src/NzbDrone.Core/Books/Model/ArtistStatusType.cs diff --git a/src/NzbDrone.Core/Music/Model/Artist.cs b/src/NzbDrone.Core/Books/Model/Author.cs similarity index 74% rename from src/NzbDrone.Core/Music/Model/Artist.cs rename to src/NzbDrone.Core/Books/Model/Author.cs index 8689d9476..9fe08ea14 100644 --- a/src/NzbDrone.Core/Music/Model/Artist.cs +++ b/src/NzbDrone.Core/Books/Model/Author.cs @@ -8,20 +8,19 @@ using NzbDrone.Core.Profiles.Qualities; namespace NzbDrone.Core.Music { - public class Artist : Entity + public class Author : Entity { - public Artist() + public Author() { Tags = new HashSet(); - Metadata = new ArtistMetadata(); + Metadata = new AuthorMetadata(); } // These correspond to columns in the Artists table - public int ArtistMetadataId { get; set; } + public int AuthorMetadataId { get; set; } public string CleanName { get; set; } public string SortName { get; set; } public bool Monitored { get; set; } - public bool AlbumFolder { get; set; } public DateTime? LastInfoSync { get; set; } public string Path { get; set; } public string RootFolderPath { get; set; } @@ -34,13 +33,15 @@ namespace NzbDrone.Core.Music // Dynamically loaded from DB [MemberwiseEqualityIgnore] - public LazyLoaded Metadata { get; set; } + public LazyLoaded Metadata { get; set; } [MemberwiseEqualityIgnore] public LazyLoaded QualityProfile { get; set; } [MemberwiseEqualityIgnore] public LazyLoaded MetadataProfile { get; set; } [MemberwiseEqualityIgnore] - public LazyLoaded> Albums { get; set; } + public LazyLoaded> Books { get; set; } + [MemberwiseEqualityIgnore] + public LazyLoaded> Series { get; set; } //compatibility properties [MemberwiseEqualityIgnore] @@ -50,28 +51,27 @@ namespace NzbDrone.Core.Music } [MemberwiseEqualityIgnore] - public string ForeignArtistId + public string ForeignAuthorId { - get { return Metadata.Value.ForeignArtistId; } set { Metadata.Value.ForeignArtistId = value; } + get { return Metadata.Value.ForeignAuthorId; } set { Metadata.Value.ForeignAuthorId = value; } } public override string ToString() { - return string.Format("[{0}][{1}]", Metadata.Value.ForeignArtistId.NullSafe(), Metadata.Value.Name.NullSafe()); + return string.Format("[{0}][{1}]", Metadata.Value.ForeignAuthorId.NullSafe(), Metadata.Value.Name.NullSafe()); } - public override void UseMetadataFrom(Artist other) + public override void UseMetadataFrom(Author other) { CleanName = other.CleanName; SortName = other.SortName; } - public override void UseDbFieldsFrom(Artist other) + public override void UseDbFieldsFrom(Author other) { Id = other.Id; - ArtistMetadataId = other.ArtistMetadataId; + AuthorMetadataId = other.AuthorMetadataId; Monitored = other.Monitored; - AlbumFolder = other.AlbumFolder; LastInfoSync = other.LastInfoSync; Path = other.Path; RootFolderPath = other.RootFolderPath; @@ -82,7 +82,7 @@ namespace NzbDrone.Core.Music AddOptions = other.AddOptions; } - public override void ApplyChanges(Artist other) + public override void ApplyChanges(Author other) { Path = other.Path; QualityProfileId = other.QualityProfileId; @@ -90,12 +90,11 @@ namespace NzbDrone.Core.Music MetadataProfileId = other.MetadataProfileId; MetadataProfile = other.MetadataProfile; - Albums = other.Albums; + Books = other.Books; Tags = other.Tags; AddOptions = other.AddOptions; RootFolderPath = other.RootFolderPath; Monitored = other.Monitored; - AlbumFolder = other.AlbumFolder; } } } diff --git a/src/NzbDrone.Core/Music/Model/ArtistMetadata.cs b/src/NzbDrone.Core/Books/Model/AuthorMetadata.cs similarity index 66% rename from src/NzbDrone.Core/Music/Model/ArtistMetadata.cs rename to src/NzbDrone.Core/Books/Model/AuthorMetadata.cs index 108dad867..fd030700b 100644 --- a/src/NzbDrone.Core/Music/Model/ArtistMetadata.cs +++ b/src/NzbDrone.Core/Books/Model/AuthorMetadata.cs @@ -4,20 +4,20 @@ using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Music { - public class ArtistMetadata : Entity + public class AuthorMetadata : Entity { - public ArtistMetadata() + public AuthorMetadata() { Images = new List(); Genres = new List(); - Members = new List(); Links = new List(); - OldForeignArtistIds = new List(); Aliases = new List(); + Ratings = new Ratings(); } - public string ForeignArtistId { get; set; } - public List OldForeignArtistIds { get; set; } + public string ForeignAuthorId { get; set; } + public int GoodreadsId { get; set; } + public string TitleSlug { get; set; } public string Name { get; set; } public List Aliases { get; set; } public string Overview { get; set; } @@ -28,17 +28,17 @@ namespace NzbDrone.Core.Music public List Links { get; set; } public List Genres { get; set; } public Ratings Ratings { get; set; } - public List Members { get; set; } public override string ToString() { - return string.Format("[{0}][{1}]", ForeignArtistId, Name.NullSafe()); + return string.Format("[{0}][{1}]", ForeignAuthorId, Name.NullSafe()); } - public override void UseMetadataFrom(ArtistMetadata other) + public override void UseMetadataFrom(AuthorMetadata other) { - ForeignArtistId = other.ForeignArtistId; - OldForeignArtistIds = other.OldForeignArtistIds; + ForeignAuthorId = other.ForeignAuthorId; + GoodreadsId = other.GoodreadsId; + TitleSlug = other.TitleSlug; Name = other.Name; Aliases = other.Aliases; Overview = other.Overview.IsNullOrWhiteSpace() ? Overview : other.Overview; @@ -48,8 +48,7 @@ namespace NzbDrone.Core.Music Images = other.Images.Any() ? other.Images : Images; Links = other.Links; Genres = other.Genres; - Ratings = other.Ratings; - Members = other.Members; + Ratings = other.Ratings.Votes > 0 ? other.Ratings : Ratings; } } } diff --git a/src/NzbDrone.Core/Music/Model/Album.cs b/src/NzbDrone.Core/Books/Model/Book.cs similarity index 59% rename from src/NzbDrone.Core/Music/Model/Album.cs rename to src/NzbDrone.Core/Books/Model/Book.cs index dcf96a4a6..27af2e3ac 100644 --- a/src/NzbDrone.Core/Music/Model/Album.cs +++ b/src/NzbDrone.Core/Books/Model/Book.cs @@ -5,45 +5,47 @@ using Equ; using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles; namespace NzbDrone.Core.Music { - public class Album : Entity + public class Book : Entity { - public Album() + public Book() { - OldForeignAlbumIds = new List(); Overview = string.Empty; Images = new List(); Links = new List(); Genres = new List(); - SecondaryTypes = new List(); Ratings = new Ratings(); - Artist = new Artist(); + Author = new Author(); AddOptions = new AddAlbumOptions(); } // These correspond to columns in the Albums table // These are metadata entries - public int ArtistMetadataId { get; set; } - public string ForeignAlbumId { get; set; } - public List OldForeignAlbumIds { get; set; } + public int AuthorMetadataId { get; set; } + public string ForeignBookId { get; set; } + public string ForeignWorkId { get; set; } + public int GoodreadsId { get; set; } + public string TitleSlug { get; set; } + public string Isbn13 { get; set; } + public string Asin { get; set; } public string Title { get; set; } + public string Language { get; set; } public string Overview { get; set; } public string Disambiguation { get; set; } + public string Publisher { get; set; } + public int PageCount { get; set; } public DateTime? ReleaseDate { get; set; } public List Images { get; set; } public List Links { get; set; } public List Genres { get; set; } - public string AlbumType { get; set; } - public List SecondaryTypes { get; set; } public Ratings Ratings { get; set; } // These are Readarr generated/config public string CleanTitle { get; set; } - public int ProfileId { get; set; } public bool Monitored { get; set; } - public bool AnyReleaseOk { get; set; } public DateTime? LastInfoSync { get; set; } public DateTime Added { get; set; } [MemberwiseEqualityIgnore] @@ -51,61 +53,65 @@ namespace NzbDrone.Core.Music // These are dynamically queried from other tables [MemberwiseEqualityIgnore] - public LazyLoaded ArtistMetadata { get; set; } + public LazyLoaded AuthorMetadata { get; set; } [MemberwiseEqualityIgnore] - public LazyLoaded> AlbumReleases { get; set; } + public LazyLoaded Author { get; set; } [MemberwiseEqualityIgnore] - public LazyLoaded Artist { get; set; } + public LazyLoaded> BookFiles { get; set; } + [MemberwiseEqualityIgnore] + public LazyLoaded> SeriesLinks { get; set; } //compatibility properties with old version of Album [MemberwiseEqualityIgnore] [JsonIgnore] - public int ArtistId + public int AuthorId { - get { return Artist?.Value?.Id ?? 0; } set { Artist.Value.Id = value; } + get { return Author?.Value?.Id ?? 0; } set { Author.Value.Id = value; } } public override string ToString() { - return string.Format("[{0}][{1}]", ForeignAlbumId, Title.NullSafe()); + return string.Format("[{0}][{1}]", ForeignBookId, Title.NullSafe()); } - public override void UseMetadataFrom(Album other) + public override void UseMetadataFrom(Book other) { - ForeignAlbumId = other.ForeignAlbumId; - OldForeignAlbumIds = other.OldForeignAlbumIds; + ForeignBookId = other.ForeignBookId; + ForeignWorkId = other.ForeignWorkId; + GoodreadsId = other.GoodreadsId; + TitleSlug = other.TitleSlug; + Isbn13 = other.Isbn13; + Asin = other.Asin; Title = other.Title; + Language = other.Language; Overview = other.Overview.IsNullOrWhiteSpace() ? Overview : other.Overview; Disambiguation = other.Disambiguation; + Publisher = other.Publisher; + PageCount = other.PageCount; ReleaseDate = other.ReleaseDate; Images = other.Images.Any() ? other.Images : Images; Links = other.Links; Genres = other.Genres; - AlbumType = other.AlbumType; - SecondaryTypes = other.SecondaryTypes; Ratings = other.Ratings; CleanTitle = other.CleanTitle; } - public override void UseDbFieldsFrom(Album other) + public override void UseDbFieldsFrom(Book other) { Id = other.Id; - ArtistMetadataId = other.ArtistMetadataId; - ProfileId = other.ProfileId; + AuthorMetadataId = other.AuthorMetadataId; Monitored = other.Monitored; - AnyReleaseOk = other.AnyReleaseOk; LastInfoSync = other.LastInfoSync; Added = other.Added; AddOptions = other.AddOptions; } - public override void ApplyChanges(Album other) + public override void ApplyChanges(Book other) { - ForeignAlbumId = other.ForeignAlbumId; - ProfileId = other.ProfileId; + ForeignBookId = other.ForeignBookId; + ForeignWorkId = other.ForeignWorkId; AddOptions = other.AddOptions; Monitored = other.Monitored; - AnyReleaseOk = other.AnyReleaseOk; } } } diff --git a/src/NzbDrone.Core/Music/Model/Entity.cs b/src/NzbDrone.Core/Books/Model/Entity.cs similarity index 100% rename from src/NzbDrone.Core/Music/Model/Entity.cs rename to src/NzbDrone.Core/Books/Model/Entity.cs diff --git a/src/NzbDrone.Core/Music/Model/Links.cs b/src/NzbDrone.Core/Books/Model/Links.cs similarity index 100% rename from src/NzbDrone.Core/Music/Model/Links.cs rename to src/NzbDrone.Core/Books/Model/Links.cs diff --git a/src/NzbDrone.Core/Music/Model/MonitoringOptions.cs b/src/NzbDrone.Core/Books/Model/MonitoringOptions.cs similarity index 100% rename from src/NzbDrone.Core/Music/Model/MonitoringOptions.cs rename to src/NzbDrone.Core/Books/Model/MonitoringOptions.cs diff --git a/src/NzbDrone.Core/Music/Model/Ratings.cs b/src/NzbDrone.Core/Books/Model/Ratings.cs similarity index 100% rename from src/NzbDrone.Core/Music/Model/Ratings.cs rename to src/NzbDrone.Core/Books/Model/Ratings.cs diff --git a/src/NzbDrone.Core/Books/Model/Series.cs b/src/NzbDrone.Core/Books/Model/Series.cs new file mode 100644 index 000000000..900e70a44 --- /dev/null +++ b/src/NzbDrone.Core/Books/Model/Series.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Equ; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Music +{ + public class Series : Entity + { + public string ForeignSeriesId { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public bool Numbered { get; set; } + public int WorkCount { get; set; } + public int PrimaryWorkCount { get; set; } + + [MemberwiseEqualityIgnore] + public LazyLoaded> LinkItems { get; set; } + + [MemberwiseEqualityIgnore] + public LazyLoaded> Books { get; set; } + + public override string ToString() + { + return string.Format("[{0}][{1}]", ForeignSeriesId.NullSafe(), Title.NullSafe()); + } + + public override void UseMetadataFrom(Series other) + { + ForeignSeriesId = other.ForeignSeriesId; + Title = other.Title; + Description = other.Description; + Numbered = other.Numbered; + WorkCount = other.WorkCount; + PrimaryWorkCount = other.PrimaryWorkCount; + } + + public override void UseDbFieldsFrom(Series other) + { + Id = other.Id; + } + } +} diff --git a/src/NzbDrone.Core/Books/Model/SeriesBookLink.cs b/src/NzbDrone.Core/Books/Model/SeriesBookLink.cs new file mode 100644 index 000000000..fa7ce65fe --- /dev/null +++ b/src/NzbDrone.Core/Books/Model/SeriesBookLink.cs @@ -0,0 +1,31 @@ +using Equ; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Music +{ + public class SeriesBookLink : Entity + { + public string Position { get; set; } + public int SeriesId { get; set; } + public int BookId { get; set; } + public bool IsPrimary { get; set; } + + [MemberwiseEqualityIgnore] + public LazyLoaded Series { get; set; } + [MemberwiseEqualityIgnore] + public LazyLoaded Book { get; set; } + + public override void UseMetadataFrom(SeriesBookLink other) + { + Position = other.Position; + IsPrimary = other.IsPrimary; + } + + public override void UseDbFieldsFrom(SeriesBookLink other) + { + Id = other.Id; + SeriesId = other.SeriesId; + BookId = other.BookId; + } + } +} diff --git a/src/NzbDrone.Core/Books/Repositories/AlbumRepository.cs b/src/NzbDrone.Core/Books/Repositories/AlbumRepository.cs new file mode 100644 index 000000000..8a2f8578a --- /dev/null +++ b/src/NzbDrone.Core/Books/Repositories/AlbumRepository.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Music +{ + public interface IAlbumRepository : IBasicRepository + { + List GetAlbums(int authorId); + List GetLastAlbums(IEnumerable artistMetadataIds); + List GetNextAlbums(IEnumerable artistMetadataIds); + List GetAlbumsByArtistMetadataId(int artistMetadataId); + List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds); + List GetAlbumsByFileIds(IEnumerable fileIds); + Book FindByTitle(int artistMetadataId, string title); + Book FindById(string foreignBookId); + Book FindBySlug(string titleSlug); + PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); + PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff); + List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored); + List ArtistAlbumsBetweenDates(Author artist, DateTime startDate, DateTime endDate, bool includeUnmonitored); + void SetMonitoredFlat(Book album, bool monitored); + void SetMonitored(IEnumerable ids, bool monitored); + List GetArtistAlbumsWithFiles(Author artist); + } + + public class AlbumRepository : BasicRepository, IAlbumRepository + { + public AlbumRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public List GetAlbums(int authorId) + { + return Query(Builder().Join((l, r) => l.AuthorMetadataId == r.AuthorMetadataId).Where(a => a.Id == authorId)); + } + + public List GetLastAlbums(IEnumerable artistMetadataIds) + { + var now = DateTime.UtcNow; + return Query(Builder().Where(x => artistMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now) + .GroupBy(x => x.AuthorMetadataId) + .Having("Books.ReleaseDate = MAX(Books.ReleaseDate)")); + } + + public List GetNextAlbums(IEnumerable artistMetadataIds) + { + var now = DateTime.UtcNow; + return Query(Builder().Where(x => artistMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now) + .GroupBy(x => x.AuthorMetadataId) + .Having("Books.ReleaseDate = MIN(Books.ReleaseDate)")); + } + + public List GetAlbumsByArtistMetadataId(int artistMetadataId) + { + return Query(s => s.AuthorMetadataId == artistMetadataId); + } + + public List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds) + { + return Query(a => a.AuthorMetadataId == artistMetadataId || foreignIds.Contains(a.ForeignBookId)); + } + + public List GetAlbumsByFileIds(IEnumerable fileIds) + { + return Query(new SqlBuilder() + .Join((l, r) => l.Id == r.BookId) + .Where(f => fileIds.Contains(f.Id))) + .DistinctBy(x => x.Id) + .ToList(); + } + + public Book FindById(string foreignBookId) + { + return Query(s => s.ForeignBookId == foreignBookId).SingleOrDefault(); + } + + public Book FindBySlug(string titleSlug) + { + return Query(s => s.TitleSlug == titleSlug).SingleOrDefault(); + } + + //x.Id == null is converted to SQL, so warning incorrect +#pragma warning disable CS0472 + private SqlBuilder AlbumsWithoutFilesBuilder(DateTime currentTime) => Builder() + .Join((l, r) => l.AuthorMetadataId == r.AuthorMetadataId) + .LeftJoin((t, f) => t.Id == f.BookId) + .Where(f => f.Id == null) + .Where(a => a.ReleaseDate <= currentTime); +#pragma warning restore CS0472 + + public PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec) + { + var currentTime = DateTime.UtcNow; + + pagingSpec.Records = GetPagedRecords(AlbumsWithoutFilesBuilder(currentTime), pagingSpec, PagedQuery); + pagingSpec.TotalRecords = GetPagedRecordCount(AlbumsWithoutFilesBuilder(currentTime).SelectCountDistinct(x => x.Id), pagingSpec); + + return pagingSpec; + } + + private SqlBuilder AlbumsWhereCutoffUnmetBuilder(List qualitiesBelowCutoff) => Builder() + .Join((l, r) => l.AuthorMetadataId == r.AuthorMetadataId) + .Join((t, f) => t.Id == f.BookId) + .Where(BuildQualityCutoffWhereClause(qualitiesBelowCutoff)); + + private string BuildQualityCutoffWhereClause(List qualitiesBelowCutoff) + { + var clauses = new List(); + + foreach (var profile in qualitiesBelowCutoff) + { + foreach (var belowCutoff in profile.QualityIds) + { + clauses.Add(string.Format("(Authors.[QualityProfileId] = {0} AND BookFiles.Quality LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); + } + } + + return string.Format("({0})", string.Join(" OR ", clauses)); + } + + public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff) + { + pagingSpec.Records = GetPagedRecords(AlbumsWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery); + + var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM {TableMapping.Mapper.TableNameMapping(typeof(Book))} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/)"; + pagingSpec.TotalRecords = GetPagedRecordCount(AlbumsWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Book)), pagingSpec, countTemplate); + + return pagingSpec; + } + + public List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored) + { + var builder = Builder().Where(rg => rg.ReleaseDate >= startDate && rg.ReleaseDate <= endDate); + + if (!includeUnmonitored) + { + builder = builder.Where(e => e.Monitored == true) + .Join((l, r) => l.AuthorMetadataId == r.AuthorMetadataId) + .Where(e => e.Monitored == true); + } + + return Query(builder); + } + + public List ArtistAlbumsBetweenDates(Author artist, DateTime startDate, DateTime endDate, bool includeUnmonitored) + { + var builder = Builder().Where(rg => rg.ReleaseDate >= startDate && + rg.ReleaseDate <= endDate && + rg.AuthorMetadataId == artist.AuthorMetadataId); + + if (!includeUnmonitored) + { + builder = builder.Where(e => e.Monitored == true) + .Join((l, r) => l.AuthorMetadataId == r.AuthorMetadataId) + .Where(e => e.Monitored == true); + } + + return Query(builder); + } + + public void SetMonitoredFlat(Book album, bool monitored) + { + album.Monitored = monitored; + SetFields(album, p => p.Monitored); + } + + public void SetMonitored(IEnumerable ids, bool monitored) + { + var albums = ids.Select(x => new Book { Id = x, Monitored = monitored }).ToList(); + SetFields(albums, p => p.Monitored); + } + + public Book FindByTitle(int artistMetadataId, string title) + { + var cleanTitle = Parser.Parser.CleanArtistName(title); + + if (string.IsNullOrEmpty(cleanTitle)) + { + cleanTitle = title; + } + + return Query(s => (s.CleanTitle == cleanTitle || s.Title == title) && s.AuthorMetadataId == artistMetadataId) + .ExclusiveOrDefault(); + } + + public List GetArtistAlbumsWithFiles(Author artist) + { + return Query(Builder() + .Join((t, f) => t.Id == f.BookId) + .Where(x => x.AuthorMetadataId == artist.AuthorMetadataId)); + } + } +} diff --git a/src/NzbDrone.Core/Music/Repositories/ArtistMetadataRepository.cs b/src/NzbDrone.Core/Books/Repositories/ArtistMetadataRepository.cs similarity index 70% rename from src/NzbDrone.Core/Music/Repositories/ArtistMetadataRepository.cs rename to src/NzbDrone.Core/Books/Repositories/ArtistMetadataRepository.cs index df8a39a06..419c2d5aa 100644 --- a/src/NzbDrone.Core/Music/Repositories/ArtistMetadataRepository.cs +++ b/src/NzbDrone.Core/Books/Repositories/ArtistMetadataRepository.cs @@ -6,13 +6,13 @@ using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Music { - public interface IArtistMetadataRepository : IBasicRepository + public interface IArtistMetadataRepository : IBasicRepository { - List FindById(List foreignIds); - bool UpsertMany(List data); + List FindById(List foreignIds); + bool UpsertMany(List data); } - public class ArtistMetadataRepository : BasicRepository, IArtistMetadataRepository + public class ArtistMetadataRepository : BasicRepository, IArtistMetadataRepository { private readonly Logger _logger; @@ -22,24 +22,27 @@ namespace NzbDrone.Core.Music _logger = logger; } - public List FindById(List foreignIds) + public List FindById(List foreignIds) { - return Query(x => Enumerable.Contains(foreignIds, x.ForeignArtistId)); + return Query(x => Enumerable.Contains(foreignIds, x.ForeignAuthorId)); } - public bool UpsertMany(List data) + public bool UpsertMany(List data) { - var existingMetadata = FindById(data.Select(x => x.ForeignArtistId).ToList()); - var updateMetadataList = new List(); - var addMetadataList = new List(); + var existingMetadata = FindById(data.Select(x => x.ForeignAuthorId).ToList()); + var updateMetadataList = new List(); + var addMetadataList = new List(); int upToDateMetadataCount = 0; foreach (var meta in data) { - var existing = existingMetadata.SingleOrDefault(x => x.ForeignArtistId == meta.ForeignArtistId); + var existing = existingMetadata.SingleOrDefault(x => x.ForeignAuthorId == meta.ForeignAuthorId); if (existing != null) { + // populate Id in remote data meta.UseDbFieldsFrom(existing); + + // responses vary, so try adding remote to what we have if (!meta.Equals(existing)) { updateMetadataList.Add(meta); diff --git a/src/NzbDrone.Core/Books/Repositories/ArtistRepository.cs b/src/NzbDrone.Core/Books/Repositories/ArtistRepository.cs new file mode 100644 index 000000000..f12c1c05d --- /dev/null +++ b/src/NzbDrone.Core/Books/Repositories/ArtistRepository.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using Dapper; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Music +{ + public interface IArtistRepository : IBasicRepository + { + bool ArtistPathExists(string path); + Author FindByName(string cleanName); + Author FindById(string foreignAuthorId); + Author GetArtistByMetadataId(int artistMetadataId); + List GetArtistByMetadataId(IEnumerable artistMetadataId); + } + + public class ArtistRepository : BasicRepository, IArtistRepository + { + public ArtistRepository(IMainDatabase database, + IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + protected override SqlBuilder Builder() => new SqlBuilder() + .Join((a, m) => a.AuthorMetadataId == m.Id); + + protected override List Query(SqlBuilder builder) => Query(_database, builder).ToList(); + + public static IEnumerable Query(IDatabase database, SqlBuilder builder) + { + return database.QueryJoined(builder, (artist, metadata) => + { + artist.Metadata = metadata; + return artist; + }); + } + + public bool ArtistPathExists(string path) + { + return Query(c => c.Path == path).Any(); + } + + public Author FindById(string foreignAuthorId) + { + return Query(Builder().Where(m => m.ForeignAuthorId == foreignAuthorId)).SingleOrDefault(); + } + + public Author FindByName(string cleanName) + { + cleanName = cleanName.ToLowerInvariant(); + + return Query(s => s.CleanName == cleanName).ExclusiveOrDefault(); + } + + public Author GetArtistByMetadataId(int artistMetadataId) + { + return Query(s => s.AuthorMetadataId == artistMetadataId).SingleOrDefault(); + } + + public List GetArtistByMetadataId(IEnumerable artistMetadataIds) + { + return Query(s => artistMetadataIds.Contains(s.AuthorMetadataId)); + } + } +} diff --git a/src/NzbDrone.Core/Books/Repositories/SeriesBookLinkRepository.cs b/src/NzbDrone.Core/Books/Repositories/SeriesBookLinkRepository.cs new file mode 100644 index 000000000..4e93804c2 --- /dev/null +++ b/src/NzbDrone.Core/Books/Repositories/SeriesBookLinkRepository.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Music +{ + public interface ISeriesBookLinkRepository : IBasicRepository + { + List GetLinksBySeries(int seriesId); + } + + public class SeriesBookLinkRepository : BasicRepository, ISeriesBookLinkRepository + { + public SeriesBookLinkRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public List GetLinksBySeries(int seriesId) + { + return Query(x => x.SeriesId == seriesId); + } + } +} diff --git a/src/NzbDrone.Core/Books/Repositories/SeriesRepository.cs b/src/NzbDrone.Core/Books/Repositories/SeriesRepository.cs new file mode 100644 index 000000000..891d323e8 --- /dev/null +++ b/src/NzbDrone.Core/Books/Repositories/SeriesRepository.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Music +{ + public interface ISeriesRepository : IBasicRepository + { + Series FindById(string foreignSeriesId); + List FindById(IEnumerable foreignSeriesId); + List GetByAuthorMetadataId(int authorMetadataId); + List GetByAuthorId(int authorId); + } + + public class SeriesRepository : BasicRepository, ISeriesRepository + { + public SeriesRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public Series FindById(string foreignSeriesId) + { + return Query(x => x.ForeignSeriesId == foreignSeriesId).SingleOrDefault(); + } + + public List FindById(IEnumerable foreignSeriesId) + { + return Query(x => foreignSeriesId.Contains(x.ForeignSeriesId)); + } + + public List GetByAuthorMetadataId(int authorMetadataId) + { + return QueryDistinct(Builder().Join((l, r) => l.Id == r.SeriesId) + .Join((l, r) => l.BookId == r.Id) + .Where(x => x.AuthorMetadataId == authorMetadataId)); + } + + public List GetByAuthorId(int authorId) + { + return QueryDistinct(Builder().Join((l, r) => l.Id == r.SeriesId) + .Join((l, r) => l.BookId == r.Id) + .Join((l, r) => l.AuthorMetadataId == r.AuthorMetadataId) + .Where(x => x.Id == authorId)); + } + } +} diff --git a/src/NzbDrone.Core/Music/Services/AddAlbumService.cs b/src/NzbDrone.Core/Books/Services/AddAlbumService.cs similarity index 61% rename from src/NzbDrone.Core/Music/Services/AddAlbumService.cs rename to src/NzbDrone.Core/Books/Services/AddAlbumService.cs index 8bee983cd..effe499c6 100644 --- a/src/NzbDrone.Core/Music/Services/AddAlbumService.cs +++ b/src/NzbDrone.Core/Books/Services/AddAlbumService.cs @@ -12,8 +12,8 @@ namespace NzbDrone.Core.Music { public interface IAddAlbumService { - Album AddAlbum(Album album); - List AddAlbums(List albums); + Book AddAlbum(Book album, bool doRefresh = true); + List AddAlbums(List albums, bool doRefresh = true); } public class AddAlbumService : IAddAlbumService @@ -21,14 +21,14 @@ namespace NzbDrone.Core.Music private readonly IArtistService _artistService; private readonly IAddArtistService _addArtistService; private readonly IAlbumService _albumService; - private readonly IProvideAlbumInfo _albumInfo; + private readonly IProvideBookInfo _albumInfo; private readonly IImportListExclusionService _importListExclusionService; private readonly Logger _logger; public AddAlbumService(IArtistService artistService, IAddArtistService addArtistService, IAlbumService albumService, - IProvideAlbumInfo albumInfo, + IProvideBookInfo albumInfo, IImportListExclusionService importListExclusionService, Logger logger) { @@ -40,72 +40,81 @@ namespace NzbDrone.Core.Music _logger = logger; } - public Album AddAlbum(Album album) + public Book AddAlbum(Book album, bool doRefresh = true) { _logger.Debug($"Adding album {album}"); album = AddSkyhookData(album); // Remove any import list exclusions preventing addition - _importListExclusionService.Delete(album.ForeignAlbumId); - _importListExclusionService.Delete(album.ArtistMetadata.Value.ForeignArtistId); + _importListExclusionService.Delete(album.ForeignBookId); + _importListExclusionService.Delete(album.AuthorMetadata.Value.ForeignAuthorId); // Note it's a manual addition so it's not deleted on next refresh album.AddOptions.AddType = AlbumAddType.Manual; // Add the artist if necessary - var dbArtist = _artistService.FindById(album.ArtistMetadata.Value.ForeignArtistId); + var dbArtist = _artistService.FindById(album.AuthorMetadata.Value.ForeignAuthorId); if (dbArtist == null) { - var artist = album.Artist.Value; + var artist = album.Author.Value; - artist.Metadata.Value.ForeignArtistId = album.ArtistMetadata.Value.ForeignArtistId; + artist.Metadata.Value.ForeignAuthorId = album.AuthorMetadata.Value.ForeignAuthorId; dbArtist = _addArtistService.AddArtist(artist, false); } - album.ArtistMetadataId = dbArtist.ArtistMetadataId; - _albumService.AddAlbum(album); + album.Author = dbArtist; + album.AuthorMetadataId = dbArtist.AuthorMetadataId; + _albumService.AddAlbum(album, doRefresh); return album; } - public List AddAlbums(List albums) + public List AddAlbums(List albums, bool doRefresh = true) { var added = DateTime.UtcNow; - var addedAlbums = new List(); + var addedAlbums = new List(); foreach (var a in albums) { a.Added = added; - addedAlbums.Add(AddAlbum(a)); + try + { + addedAlbums.Add(AddAlbum(a, doRefresh)); + } + catch (Exception ex) + { + // Could be a bad id from an import list + _logger.Error(ex, "Failed to import id: {0} - {1}", a.ForeignBookId, a.Title); + } } return addedAlbums; } - private Album AddSkyhookData(Album newAlbum) + private Book AddSkyhookData(Book newAlbum) { - Tuple> tuple = null; + Tuple> tuple = null; try { - tuple = _albumInfo.GetAlbumInfo(newAlbum.ForeignAlbumId); + tuple = _albumInfo.GetBookInfo(newAlbum.ForeignBookId); } catch (AlbumNotFoundException) { - _logger.Error("Album with MusicBrainz Id {0} was not found, it may have been removed from Musicbrainz.", newAlbum.ForeignAlbumId); + _logger.Error("Album with MusicBrainz Id {0} was not found, it may have been removed from Musicbrainz.", newAlbum.ForeignBookId); throw new ValidationException(new List { - new ValidationFailure("MusicbrainzId", "An album with this ID was not found", newAlbum.ForeignAlbumId) + new ValidationFailure("MusicbrainzId", "An album with this ID was not found", newAlbum.ForeignBookId) }); } newAlbum.UseMetadataFrom(tuple.Item2); newAlbum.Added = DateTime.UtcNow; - var metadata = tuple.Item3.Single(x => x.ForeignArtistId == tuple.Item1); - newAlbum.ArtistMetadata = metadata; + var metadata = tuple.Item3.Single(x => x.ForeignAuthorId == tuple.Item1); + newAlbum.AuthorMetadata = metadata; return newAlbum; } diff --git a/src/NzbDrone.Core/Music/Services/AddArtistService.cs b/src/NzbDrone.Core/Books/Services/AddAuthorService.cs similarity index 83% rename from src/NzbDrone.Core/Music/Services/AddArtistService.cs rename to src/NzbDrone.Core/Books/Services/AddAuthorService.cs index a9829fe6e..996b78609 100644 --- a/src/NzbDrone.Core/Music/Services/AddArtistService.cs +++ b/src/NzbDrone.Core/Books/Services/AddAuthorService.cs @@ -16,22 +16,22 @@ namespace NzbDrone.Core.Music { public interface IAddArtistService { - Artist AddArtist(Artist newArtist, bool doRefresh = true); - List AddArtists(List newArtists); + Author AddArtist(Author newArtist, bool doRefresh = true); + List AddArtists(List newArtists, bool doRefresh = true); } public class AddArtistService : IAddArtistService { private readonly IArtistService _artistService; private readonly IArtistMetadataService _artistMetadataService; - private readonly IProvideArtistInfo _artistInfo; + private readonly IProvideAuthorInfo _artistInfo; private readonly IBuildFileNames _fileNameBuilder; private readonly IAddArtistValidator _addArtistValidator; private readonly Logger _logger; public AddArtistService(IArtistService artistService, IArtistMetadataService artistMetadataService, - IProvideArtistInfo artistInfo, + IProvideAuthorInfo artistInfo, IBuildFileNames fileNameBuilder, IAddArtistValidator addArtistValidator, Logger logger) @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Music _logger = logger; } - public Artist AddArtist(Artist newArtist, bool doRefresh = true) + public Author AddArtist(Author newArtist, bool doRefresh = true) { Ensure.That(newArtist, () => newArtist).IsNotNull(); @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Music // add metadata _artistMetadataService.Upsert(newArtist.Metadata.Value); - newArtist.ArtistMetadataId = newArtist.Metadata.Value.Id; + newArtist.AuthorMetadataId = newArtist.Metadata.Value.Id; // add the artist itself _artistService.AddArtist(newArtist, doRefresh); @@ -63,10 +63,10 @@ namespace NzbDrone.Core.Music return newArtist; } - public List AddArtists(List newArtists) + public List AddArtists(List newArtists, bool doRefresh = true) { var added = DateTime.UtcNow; - var artistsToAdd = new List(); + var artistsToAdd = new List(); foreach (var s in newArtists) { @@ -80,32 +80,32 @@ namespace NzbDrone.Core.Music catch (Exception ex) { // Catch Import Errors for now until we get things fixed up - _logger.Error(ex, "Failed to import id: {0} - {1}", s.Metadata.Value.ForeignArtistId, s.Metadata.Value.Name); + _logger.Error(ex, "Failed to import id: {0} - {1}", s.Metadata.Value.ForeignAuthorId, s.Metadata.Value.Name); } } // add metadata _artistMetadataService.UpsertMany(artistsToAdd.Select(x => x.Metadata.Value).ToList()); - artistsToAdd.ForEach(x => x.ArtistMetadataId = x.Metadata.Value.Id); + artistsToAdd.ForEach(x => x.AuthorMetadataId = x.Metadata.Value.Id); - return _artistService.AddArtists(artistsToAdd); + return _artistService.AddArtists(artistsToAdd, doRefresh); } - private Artist AddSkyhookData(Artist newArtist) + private Author AddSkyhookData(Author newArtist) { - Artist artist; + Author artist; try { - artist = _artistInfo.GetArtistInfo(newArtist.Metadata.Value.ForeignArtistId, newArtist.MetadataProfileId); + artist = _artistInfo.GetAuthorInfo(newArtist.Metadata.Value.ForeignAuthorId); } catch (ArtistNotFoundException) { - _logger.Error("ReadarrId {0} was not found, it may have been removed from Musicbrainz.", newArtist.Metadata.Value.ForeignArtistId); + _logger.Error("ReadarrId {0} was not found, it may have been removed from Musicbrainz.", newArtist.Metadata.Value.ForeignAuthorId); throw new ValidationException(new List { - new ValidationFailure("MusicbrainzId", "An artist with this ID was not found", newArtist.Metadata.Value.ForeignArtistId) + new ValidationFailure("MusicbrainzId", "An artist with this ID was not found", newArtist.Metadata.Value.ForeignAuthorId) }); } @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Music return artist; } - private Artist SetPropertiesAndValidate(Artist newArtist) + private Author SetPropertiesAndValidate(Author newArtist) { var path = newArtist.Path; if (string.IsNullOrWhiteSpace(path)) diff --git a/src/NzbDrone.Core/Music/Services/AlbumAddedService.cs b/src/NzbDrone.Core/Books/Services/AlbumAddedService.cs similarity index 90% rename from src/NzbDrone.Core/Music/Services/AlbumAddedService.cs rename to src/NzbDrone.Core/Books/Services/AlbumAddedService.cs index 4568614f8..6841b8355 100644 --- a/src/NzbDrone.Core/Music/Services/AlbumAddedService.cs +++ b/src/NzbDrone.Core/Books/Services/AlbumAddedService.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Music { public interface IAlbumAddedService { - void SearchForRecentlyAdded(int artistId); + void SearchForRecentlyAdded(int authorId); } public class AlbumAddedService : IHandle, IAlbumAddedService @@ -34,9 +34,9 @@ namespace NzbDrone.Core.Music _addedAlbumsCache = cacheManager.GetCache>(GetType()); } - public void SearchForRecentlyAdded(int artistId) + public void SearchForRecentlyAdded(int authorId) { - var allAlbums = _albumService.GetAlbumsByArtist(artistId); + var allAlbums = _albumService.GetAlbumsByArtist(authorId); var toSearch = allAlbums.Where(x => x.AddOptions.SearchForNewAlbum).ToList(); if (toSearch.Any()) @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Music _albumService.SetAddOptions(toSearch); } - var recentlyAddedIds = _addedAlbumsCache.Find(artistId.ToString()); + var recentlyAddedIds = _addedAlbumsCache.Find(authorId.ToString()); if (recentlyAddedIds != null) { toSearch.AddRange(allAlbums.Where(x => recentlyAddedIds.Contains(x.Id))); @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Music _commandQueueManager.Push(new AlbumSearchCommand(toSearch.Select(e => e.Id).ToList())); } - _addedAlbumsCache.Remove(artistId.ToString()); + _addedAlbumsCache.Remove(authorId.ToString()); } public void Handle(AlbumInfoRefreshedEvent message) diff --git a/src/NzbDrone.Core/Music/Services/AlbumCutoffService.cs b/src/NzbDrone.Core/Books/Services/AlbumCutoffService.cs similarity index 89% rename from src/NzbDrone.Core/Music/Services/AlbumCutoffService.cs rename to src/NzbDrone.Core/Books/Services/AlbumCutoffService.cs index a83252fcb..31d877cf1 100644 --- a/src/NzbDrone.Core/Music/Services/AlbumCutoffService.cs +++ b/src/NzbDrone.Core/Books/Services/AlbumCutoffService.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Music { public interface IAlbumCutoffService { - PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec); + PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec); } public class AlbumCutoffService : IAlbumCutoffService @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Music _profileService = profileService; } - public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec) + public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec) { var qualitiesBelowCutoff = new List(); var profiles = _profileService.All(); diff --git a/src/NzbDrone.Core/Music/Services/AlbumMonitoredService.cs b/src/NzbDrone.Core/Books/Services/AlbumMonitoredService.cs similarity index 93% rename from src/NzbDrone.Core/Music/Services/AlbumMonitoredService.cs rename to src/NzbDrone.Core/Books/Services/AlbumMonitoredService.cs index 137d0ac55..84fd75330 100644 --- a/src/NzbDrone.Core/Music/Services/AlbumMonitoredService.cs +++ b/src/NzbDrone.Core/Books/Services/AlbumMonitoredService.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Music { public interface IAlbumMonitoredService { - void SetAlbumMonitoredStatus(Artist artist, MonitoringOptions monitoringOptions); + void SetAlbumMonitoredStatus(Author artist, MonitoringOptions monitoringOptions); } public class AlbumMonitoredService : IAlbumMonitoredService @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Music _logger = logger; } - public void SetAlbumMonitoredStatus(Artist artist, MonitoringOptions monitoringOptions) + public void SetAlbumMonitoredStatus(Author artist, MonitoringOptions monitoringOptions) { if (monitoringOptions != null) { @@ -41,9 +41,9 @@ namespace NzbDrone.Core.Music if (monitoredAlbums.Any()) { ToggleAlbumsMonitoredState( - albums.Where(s => monitoredAlbums.Any(t => t == s.ForeignAlbumId)), true); + albums.Where(s => monitoredAlbums.Any(t => t == s.ForeignBookId)), true); ToggleAlbumsMonitoredState( - albums.Where(s => monitoredAlbums.Any(t => t != s.ForeignAlbumId)), false); + albums.Where(s => monitoredAlbums.Any(t => t != s.ForeignBookId)), false); } else { @@ -92,7 +92,7 @@ namespace NzbDrone.Core.Music _artistService.UpdateArtist(artist); } - private void ToggleAlbumsMonitoredState(IEnumerable albums, bool monitored) + private void ToggleAlbumsMonitoredState(IEnumerable albums, bool monitored) { foreach (var album in albums) { diff --git a/src/NzbDrone.Core/Music/Services/AlbumService.cs b/src/NzbDrone.Core/Books/Services/AlbumService.cs similarity index 57% rename from src/NzbDrone.Core/Music/Services/AlbumService.cs rename to src/NzbDrone.Core/Books/Services/AlbumService.cs index f6bdad2bc..6a16cf300 100644 --- a/src/NzbDrone.Core/Music/Services/AlbumService.cs +++ b/src/NzbDrone.Core/Books/Services/AlbumService.cs @@ -12,33 +12,33 @@ namespace NzbDrone.Core.Music { public interface IAlbumService { - Album GetAlbum(int albumId); - List GetAlbums(IEnumerable albumIds); - List GetAlbumsByArtist(int artistId); - List GetNextAlbumsByArtistMetadataId(IEnumerable artistMetadataIds); - List GetLastAlbumsByArtistMetadataId(IEnumerable artistMetadataIds); - List GetAlbumsByArtistMetadataId(int artistMetadataId); - List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds); - Album AddAlbum(Album newAlbum); - Album FindById(string foreignId); - Album FindByTitle(int artistMetadataId, string title); - Album FindByTitleInexact(int artistMetadataId, string title); - List GetCandidates(int artistMetadataId, string title); - void DeleteAlbum(int albumId, bool deleteFiles, bool addImportListExclusion = false); - List GetAllAlbums(); - Album UpdateAlbum(Album album); - void SetAlbumMonitored(int albumId, bool monitored); + Book GetAlbum(int bookId); + List GetAlbums(IEnumerable bookIds); + List GetAlbumsByArtist(int authorId); + List GetNextAlbumsByArtistMetadataId(IEnumerable artistMetadataIds); + List GetLastAlbumsByArtistMetadataId(IEnumerable artistMetadataIds); + List GetAlbumsByArtistMetadataId(int artistMetadataId); + List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds); + List GetAlbumsByFileIds(IEnumerable fileIds); + Book AddAlbum(Book newAlbum, bool doRefresh = true); + Book FindById(string foreignId); + Book FindBySlug(string titleSlug); + Book FindByTitle(int artistMetadataId, string title); + Book FindByTitleInexact(int artistMetadataId, string title); + List GetCandidates(int artistMetadataId, string title); + void DeleteAlbum(int bookId, bool deleteFiles, bool addImportListExclusion = false); + List GetAllAlbums(); + Book UpdateAlbum(Book album); + void SetAlbumMonitored(int bookId, bool monitored); void SetMonitored(IEnumerable ids, bool monitored); - PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); - List AlbumsBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); - List ArtistAlbumsBetweenDates(Artist artist, DateTime start, DateTime end, bool includeUnmonitored); - void InsertMany(List albums); - void UpdateMany(List albums); - void DeleteMany(List albums); - void SetAddOptions(IEnumerable albums); - Album FindAlbumByRelease(string albumReleaseId); - Album FindAlbumByTrackId(int trackId); - List GetArtistAlbumsWithFiles(Artist artist); + PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); + List AlbumsBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); + List ArtistAlbumsBetweenDates(Author artist, DateTime start, DateTime end, bool includeUnmonitored); + void InsertMany(List albums); + void UpdateMany(List albums); + void DeleteMany(List albums); + void SetAddOptions(IEnumerable albums); + List GetArtistAlbumsWithFiles(Author artist); } public class AlbumService : IAlbumService, @@ -57,37 +57,42 @@ namespace NzbDrone.Core.Music _logger = logger; } - public Album AddAlbum(Album newAlbum) + public Book AddAlbum(Book newAlbum, bool doRefresh = true) { _albumRepository.Insert(newAlbum); - _eventAggregator.PublishEvent(new AlbumAddedEvent(GetAlbum(newAlbum.Id))); + _eventAggregator.PublishEvent(new AlbumAddedEvent(GetAlbum(newAlbum.Id), doRefresh)); return newAlbum; } - public void DeleteAlbum(int albumId, bool deleteFiles, bool addImportListExclusion = false) + public void DeleteAlbum(int bookId, bool deleteFiles, bool addImportListExclusion = false) { - var album = _albumRepository.Get(albumId); - album.Artist.LazyLoad(); - _albumRepository.Delete(albumId); + var album = _albumRepository.Get(bookId); + album.Author.LazyLoad(); + _albumRepository.Delete(bookId); _eventAggregator.PublishEvent(new AlbumDeletedEvent(album, deleteFiles, addImportListExclusion)); } - public Album FindById(string foreignId) + public Book FindById(string foreignId) { return _albumRepository.FindById(foreignId); } - public Album FindByTitle(int artistMetadataId, string title) + public Book FindBySlug(string titleSlug) + { + return _albumRepository.FindBySlug(titleSlug); + } + + public Book FindByTitle(int artistMetadataId, string title) { return _albumRepository.FindByTitle(artistMetadataId, title); } - private List, string>> AlbumScoringFunctions(string title, string cleanTitle) + private List, string>> AlbumScoringFunctions(string title, string cleanTitle) { - Func, string, Tuple, string>> tc = Tuple.Create; - var scoringFunctions = new List, string>> + Func, string, Tuple, string>> tc = Tuple.Create; + var scoringFunctions = new List, string>> { tc((a, t) => a.CleanTitle.FuzzyMatch(t), cleanTitle), tc((a, t) => a.Title.FuzzyMatch(t), title), @@ -101,7 +106,7 @@ namespace NzbDrone.Core.Music return scoringFunctions; } - public Album FindByTitleInexact(int artistMetadataId, string title) + public Book FindByTitleInexact(int artistMetadataId, string title) { var albums = GetAlbumsByArtistMetadataId(artistMetadataId); @@ -117,10 +122,10 @@ namespace NzbDrone.Core.Music return null; } - public List GetCandidates(int artistMetadataId, string title) + public List GetCandidates(int artistMetadataId, string title) { var albums = GetAlbumsByArtistMetadataId(artistMetadataId); - var output = new List(); + var output = new List(); foreach (var func in AlbumScoringFunctions(title, title.CleanArtistName())) { @@ -130,7 +135,7 @@ namespace NzbDrone.Core.Music return output.DistinctBy(x => x.Id).ToList(); } - private List FindByStringInexact(List albums, Func scoreFunction, string title) + private List FindByStringInexact(List albums, Func scoreFunction, string title) { const double fuzzThreshold = 0.7; const double fuzzGap = 0.4; @@ -150,98 +155,93 @@ namespace NzbDrone.Core.Music .ToList(); } - public List GetAllAlbums() + public List GetAllAlbums() { return _albumRepository.All().ToList(); } - public Album GetAlbum(int albumId) + public Book GetAlbum(int bookId) { - return _albumRepository.Get(albumId); + return _albumRepository.Get(bookId); } - public List GetAlbums(IEnumerable albumIds) + public List GetAlbums(IEnumerable bookIds) { - return _albumRepository.Get(albumIds).ToList(); + return _albumRepository.Get(bookIds).ToList(); } - public List GetAlbumsByArtist(int artistId) + public List GetAlbumsByArtist(int authorId) { - return _albumRepository.GetAlbums(artistId).ToList(); + return _albumRepository.GetAlbums(authorId).ToList(); } - public List GetNextAlbumsByArtistMetadataId(IEnumerable artistMetadataIds) + public List GetNextAlbumsByArtistMetadataId(IEnumerable artistMetadataIds) { return _albumRepository.GetNextAlbums(artistMetadataIds).ToList(); } - public List GetLastAlbumsByArtistMetadataId(IEnumerable artistMetadataIds) + public List GetLastAlbumsByArtistMetadataId(IEnumerable artistMetadataIds) { return _albumRepository.GetLastAlbums(artistMetadataIds).ToList(); } - public List GetAlbumsByArtistMetadataId(int artistMetadataId) + public List GetAlbumsByArtistMetadataId(int artistMetadataId) { return _albumRepository.GetAlbumsByArtistMetadataId(artistMetadataId).ToList(); } - public List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds) + public List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds) { return _albumRepository.GetAlbumsForRefresh(artistMetadataId, foreignIds); } - public Album FindAlbumByRelease(string albumReleaseId) + public List GetAlbumsByFileIds(IEnumerable fileIds) { - return _albumRepository.FindAlbumByRelease(albumReleaseId); + return _albumRepository.GetAlbumsByFileIds(fileIds); } - public Album FindAlbumByTrackId(int trackId) - { - return _albumRepository.FindAlbumByTrack(trackId); - } - - public void SetAddOptions(IEnumerable albums) + public void SetAddOptions(IEnumerable albums) { _albumRepository.SetFields(albums.ToList(), s => s.AddOptions); } - public PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec) + public PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec) { var albumResult = _albumRepository.AlbumsWithoutFiles(pagingSpec); return albumResult; } - public List AlbumsBetweenDates(DateTime start, DateTime end, bool includeUnmonitored) + public List AlbumsBetweenDates(DateTime start, DateTime end, bool includeUnmonitored) { var albums = _albumRepository.AlbumsBetweenDates(start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored); return albums; } - public List ArtistAlbumsBetweenDates(Artist artist, DateTime start, DateTime end, bool includeUnmonitored) + public List ArtistAlbumsBetweenDates(Author artist, DateTime start, DateTime end, bool includeUnmonitored) { var albums = _albumRepository.ArtistAlbumsBetweenDates(artist, start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored); return albums; } - public List GetArtistAlbumsWithFiles(Artist artist) + public List GetArtistAlbumsWithFiles(Author artist) { return _albumRepository.GetArtistAlbumsWithFiles(artist); } - public void InsertMany(List albums) + public void InsertMany(List albums) { _albumRepository.InsertMany(albums); } - public void UpdateMany(List albums) + public void UpdateMany(List albums) { _albumRepository.UpdateMany(albums); } - public void DeleteMany(List albums) + public void DeleteMany(List albums) { _albumRepository.DeleteMany(albums); @@ -251,31 +251,25 @@ namespace NzbDrone.Core.Music } } - public Album UpdateAlbum(Album album) + public Book UpdateAlbum(Book album) { var storedAlbum = GetAlbum(album.Id); var updatedAlbum = _albumRepository.Update(album); - // If updatedAlbum has populated the Releases, populate in the storedAlbum too - if (updatedAlbum.AlbumReleases.IsLoaded) - { - storedAlbum.AlbumReleases.LazyLoad(); - } - _eventAggregator.PublishEvent(new AlbumEditedEvent(updatedAlbum, storedAlbum)); return updatedAlbum; } - public void SetAlbumMonitored(int albumId, bool monitored) + public void SetAlbumMonitored(int bookId, bool monitored) { - var album = _albumRepository.Get(albumId); + var album = _albumRepository.Get(bookId); _albumRepository.SetMonitoredFlat(album, monitored); // publish album edited event so artist stats update _eventAggregator.PublishEvent(new AlbumEditedEvent(album, album)); - _logger.Debug("Monitored flag for Album:{0} was set to {1}", albumId, monitored); + _logger.Debug("Monitored flag for Album:{0} was set to {1}", bookId, monitored); } public void SetMonitored(IEnumerable ids, bool monitored) @@ -291,7 +285,7 @@ namespace NzbDrone.Core.Music public void Handle(ArtistDeletedEvent message) { - var albums = GetAlbumsByArtistMetadataId(message.Artist.ArtistMetadataId); + var albums = GetAlbumsByArtistMetadataId(message.Artist.AuthorMetadataId); DeleteMany(albums); } } diff --git a/src/NzbDrone.Core/Music/Services/ArtistMetadataService.cs b/src/NzbDrone.Core/Books/Services/ArtistMetadataService.cs similarity index 72% rename from src/NzbDrone.Core/Music/Services/ArtistMetadataService.cs rename to src/NzbDrone.Core/Books/Services/ArtistMetadataService.cs index 1afc91732..6b02f908c 100644 --- a/src/NzbDrone.Core/Music/Services/ArtistMetadataService.cs +++ b/src/NzbDrone.Core/Books/Services/ArtistMetadataService.cs @@ -4,8 +4,8 @@ namespace NzbDrone.Core.Music { public interface IArtistMetadataService { - bool Upsert(ArtistMetadata artist); - bool UpsertMany(List artists); + bool Upsert(AuthorMetadata artist); + bool UpsertMany(List artists); } public class ArtistMetadataService : IArtistMetadataService @@ -17,12 +17,12 @@ namespace NzbDrone.Core.Music _artistMetadataRepository = artistMetadataRepository; } - public bool Upsert(ArtistMetadata artist) + public bool Upsert(AuthorMetadata artist) { - return _artistMetadataRepository.UpsertMany(new List { artist }); + return _artistMetadataRepository.UpsertMany(new List { artist }); } - public bool UpsertMany(List artists) + public bool UpsertMany(List artists) { return _artistMetadataRepository.UpsertMany(artists); } diff --git a/src/NzbDrone.Core/Music/Services/ArtistService.cs b/src/NzbDrone.Core/Books/Services/ArtistService.cs similarity index 62% rename from src/NzbDrone.Core/Music/Services/ArtistService.cs rename to src/NzbDrone.Core/Books/Services/ArtistService.cs index f69206f1b..d29663539 100644 --- a/src/NzbDrone.Core/Music/Services/ArtistService.cs +++ b/src/NzbDrone.Core/Books/Services/ArtistService.cs @@ -12,22 +12,23 @@ namespace NzbDrone.Core.Music { public interface IArtistService { - Artist GetArtist(int artistId); - Artist GetArtistByMetadataId(int artistMetadataId); - List GetArtists(IEnumerable artistIds); - Artist AddArtist(Artist newArtist, bool doRefresh); - List AddArtists(List newArtists); - Artist FindById(string foreignArtistId); - Artist FindByName(string title); - Artist FindByNameInexact(string title); - List GetCandidates(string title); - void DeleteArtist(int artistId, bool deleteFiles, bool addImportListExclusion = false); - List GetAllArtists(); - List AllForTag(int tagId); - Artist UpdateArtist(Artist artist); - List UpdateArtists(List artist, bool useExistingRelativeFolder); + Author GetArtist(int authorId); + Author GetArtistByMetadataId(int artistMetadataId); + List GetArtists(IEnumerable authorIds); + Author AddArtist(Author newArtist, bool doRefresh); + List AddArtists(List newArtists, bool doRefresh); + Author FindById(string foreignAuthorId); + Author FindByName(string title); + Author FindByNameInexact(string title); + List GetCandidates(string title); + List GetReportCandidates(string reportTitle); + void DeleteArtist(int authorId, bool deleteFiles, bool addImportListExclusion = false); + List GetAllArtists(); + List AllForTag(int tagId); + Author UpdateArtist(Author artist); + List UpdateArtists(List artist, bool useExistingRelativeFolder); bool ArtistPathExists(string folder); - void RemoveAddOptions(Artist artist); + void RemoveAddOptions(Author artist); } public class ArtistService : IArtistService @@ -36,7 +37,7 @@ namespace NzbDrone.Core.Music private readonly IEventAggregator _eventAggregator; private readonly IBuildArtistPaths _artistPathBuilder; private readonly Logger _logger; - private readonly ICached> _cache; + private readonly ICached> _cache; public ArtistService(IArtistRepository artistRepository, IEventAggregator eventAggregator, @@ -47,11 +48,11 @@ namespace NzbDrone.Core.Music _artistRepository = artistRepository; _eventAggregator = eventAggregator; _artistPathBuilder = artistPathBuilder; - _cache = cacheManager.GetCache>(GetType()); + _cache = cacheManager.GetCache>(GetType()); _logger = logger; } - public Artist AddArtist(Artist newArtist, bool doRefresh) + public Author AddArtist(Author newArtist, bool doRefresh) { _cache.Clear(); _artistRepository.Insert(newArtist); @@ -60,11 +61,11 @@ namespace NzbDrone.Core.Music return newArtist; } - public List AddArtists(List newArtists) + public List AddArtists(List newArtists, bool doRefresh) { _cache.Clear(); _artistRepository.InsertMany(newArtists); - _eventAggregator.PublishEvent(new ArtistsImportedEvent(newArtists.Select(s => s.Id).ToList())); + _eventAggregator.PublishEvent(new ArtistsImportedEvent(newArtists.Select(s => s.Id).ToList(), doRefresh)); return newArtists; } @@ -74,28 +75,28 @@ namespace NzbDrone.Core.Music return _artistRepository.ArtistPathExists(folder); } - public void DeleteArtist(int artistId, bool deleteFiles, bool addImportListExclusion = false) + public void DeleteArtist(int authorId, bool deleteFiles, bool addImportListExclusion = false) { _cache.Clear(); - var artist = _artistRepository.Get(artistId); - _artistRepository.Delete(artistId); + var artist = _artistRepository.Get(authorId); + _artistRepository.Delete(authorId); _eventAggregator.PublishEvent(new ArtistDeletedEvent(artist, deleteFiles, addImportListExclusion)); } - public Artist FindById(string foreignArtistId) + public Author FindById(string foreignAuthorId) { - return _artistRepository.FindById(foreignArtistId); + return _artistRepository.FindById(foreignAuthorId); } - public Artist FindByName(string title) + public Author FindByName(string title) { return _artistRepository.FindByName(title.CleanArtistName()); } - public List, string>> ArtistScoringFunctions(string title, string cleanTitle) + public List, string>> ArtistScoringFunctions(string title, string cleanTitle) { - Func, string, Tuple, string>> tc = Tuple.Create; - var scoringFunctions = new List, string>> + Func, string, Tuple, string>> tc = Tuple.Create; + var scoringFunctions = new List, string>> { tc((a, t) => a.CleanName.FuzzyMatch(t), cleanTitle), tc((a, t) => a.Name.FuzzyMatch(t), title), @@ -114,7 +115,7 @@ namespace NzbDrone.Core.Music return scoringFunctions; } - public Artist FindByNameInexact(string title) + public Author FindByNameInexact(string title) { var artists = GetAllArtists(); @@ -130,10 +131,10 @@ namespace NzbDrone.Core.Music return null; } - public List GetCandidates(string title) + public List GetCandidates(string title) { var artists = GetAllArtists(); - var output = new List(); + var output = new List(); foreach (var func in ArtistScoringFunctions(title, title.CleanArtistName())) { @@ -143,7 +144,32 @@ namespace NzbDrone.Core.Music return output.DistinctBy(x => x.Id).ToList(); } - private List FindByStringInexact(List artists, Func scoreFunction, string title) + public List, string>> ReportArtistScoringFunctions(string reportTitle, string cleanReportTitle) + { + Func, string, Tuple, string>> tc = Tuple.Create; + var scoringFunctions = new List, string>> + { + tc((a, t) => t.FuzzyContains(a.CleanName), cleanReportTitle), + tc((a, t) => t.FuzzyContains(a.Metadata.Value.Name), reportTitle) + }; + + return scoringFunctions; + } + + public List GetReportCandidates(string reportTitle) + { + var artists = GetAllArtists(); + var output = new List(); + + foreach (var func in ArtistScoringFunctions(reportTitle, reportTitle.CleanArtistName())) + { + output.AddRange(FindByStringInexact(artists, func.Item1, func.Item2)); + } + + return output.DistinctBy(x => x.Id).ToList(); + } + + private List FindByStringInexact(List artists, Func scoreFunction, string title) { const double fuzzThreshold = 0.8; const double fuzzGap = 0.2; @@ -163,38 +189,38 @@ namespace NzbDrone.Core.Music .ToList(); } - public List GetAllArtists() + public List GetAllArtists() { return _cache.Get("GetAllArtists", () => _artistRepository.All().ToList(), TimeSpan.FromSeconds(30)); } - public List AllForTag(int tagId) + public List AllForTag(int tagId) { return GetAllArtists().Where(s => s.Tags.Contains(tagId)) .ToList(); } - public Artist GetArtist(int artistId) + public Author GetArtist(int authorId) { - return _artistRepository.Get(artistId); + return _artistRepository.Get(authorId); } - public Artist GetArtistByMetadataId(int artistMetadataId) + public Author GetArtistByMetadataId(int artistMetadataId) { return _artistRepository.GetArtistByMetadataId(artistMetadataId); } - public List GetArtists(IEnumerable artistIds) + public List GetArtists(IEnumerable authorIds) { - return _artistRepository.Get(artistIds).ToList(); + return _artistRepository.Get(authorIds).ToList(); } - public void RemoveAddOptions(Artist artist) + public void RemoveAddOptions(Author artist) { _artistRepository.SetFields(artist, s => s.AddOptions); } - public Artist UpdateArtist(Artist artist) + public Author UpdateArtist(Author artist) { _cache.Clear(); var storedArtist = GetArtist(artist.Id); @@ -204,7 +230,7 @@ namespace NzbDrone.Core.Music return updatedArtist; } - public List UpdateArtists(List artist, bool useExistingRelativeFolder) + public List UpdateArtists(List artist, bool useExistingRelativeFolder) { _cache.Clear(); _logger.Debug("Updating {0} artist", artist.Count); diff --git a/src/NzbDrone.Core/Music/Services/MoveArtistService.cs b/src/NzbDrone.Core/Books/Services/MoveArtistService.cs similarity index 93% rename from src/NzbDrone.Core/Music/Services/MoveArtistService.cs rename to src/NzbDrone.Core/Books/Services/MoveArtistService.cs index 0c712a14c..da9613ffc 100644 --- a/src/NzbDrone.Core/Music/Services/MoveArtistService.cs +++ b/src/NzbDrone.Core/Books/Services/MoveArtistService.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Music _logger = logger; } - private void MoveSingleArtist(Artist artist, string sourcePath, string destinationPath, int? index = null, int? total = null) + private void MoveSingleArtist(Author artist, string sourcePath, string destinationPath, int? index = null, int? total = null) { if (!_diskProvider.FolderExists(sourcePath)) { @@ -73,9 +73,9 @@ namespace NzbDrone.Core.Music } } - private void RevertPath(int artistId, string path) + private void RevertPath(int authorId, string path) { - var artist = _artistService.GetArtist(artistId); + var artist = _artistService.GetArtist(authorId); artist.Path = path; _artistService.UpdateArtist(artist); @@ -83,7 +83,7 @@ namespace NzbDrone.Core.Music public void Execute(MoveArtistCommand message) { - var artist = _artistService.GetArtist(message.ArtistId); + var artist = _artistService.GetArtist(message.AuthorId); MoveSingleArtist(artist, message.SourcePath, message.DestinationPath); } @@ -97,7 +97,7 @@ namespace NzbDrone.Core.Music for (var index = 0; index < artistToMove.Count; index++) { var s = artistToMove[index]; - var artist = _artistService.GetArtist(s.ArtistId); + var artist = _artistService.GetArtist(s.AuthorId); var destinationPath = Path.Combine(destinationRootFolder, _filenameBuilder.GetArtistFolder(artist)); MoveSingleArtist(artist, s.SourcePath, destinationPath, index, artistToMove.Count); diff --git a/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs b/src/NzbDrone.Core/Books/Services/RefreshAuthorService.cs similarity index 66% rename from src/NzbDrone.Core/Music/Services/RefreshArtistService.cs rename to src/NzbDrone.Core/Books/Services/RefreshAuthorService.cs index 987d98d4e..b32d0a9c4 100644 --- a/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs +++ b/src/NzbDrone.Core/Books/Services/RefreshAuthorService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NLog; -using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; @@ -17,18 +16,21 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Music.Commands; using NzbDrone.Core.Music.Events; +using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.RootFolders; namespace NzbDrone.Core.Music { - public class RefreshArtistService : RefreshEntityServiceBase, + public class RefreshArtistService : RefreshEntityServiceBase, IExecute, IExecute { - private readonly IProvideArtistInfo _artistInfo; + private readonly IProvideAuthorInfo _artistInfo; private readonly IArtistService _artistService; private readonly IAlbumService _albumService; + private readonly IMetadataProfileService _metadataProfileService; private readonly IRefreshAlbumService _refreshAlbumService; + private readonly IRefreshSeriesService _refreshSeriesService; private readonly IEventAggregator _eventAggregator; private readonly IManageCommandQueue _commandQueueManager; private readonly IMediaFileService _mediaFileService; @@ -39,11 +41,13 @@ namespace NzbDrone.Core.Music private readonly IImportListExclusionService _importListExclusionService; private readonly Logger _logger; - public RefreshArtistService(IProvideArtistInfo artistInfo, + public RefreshArtistService(IProvideAuthorInfo artistInfo, IArtistService artistService, IArtistMetadataService artistMetadataService, IAlbumService albumService, + IMetadataProfileService metadataProfileService, IRefreshAlbumService refreshAlbumService, + IRefreshSeriesService refreshSeriesService, IEventAggregator eventAggregator, IManageCommandQueue commandQueueManager, IMediaFileService mediaFileService, @@ -58,7 +62,9 @@ namespace NzbDrone.Core.Music _artistInfo = artistInfo; _artistService = artistService; _albumService = albumService; + _metadataProfileService = metadataProfileService; _refreshAlbumService = refreshAlbumService; + _refreshSeriesService = refreshSeriesService; _eventAggregator = eventAggregator; _commandQueueManager = commandQueueManager; _mediaFileService = mediaFileService; @@ -70,40 +76,52 @@ namespace NzbDrone.Core.Music _logger = logger; } - protected override RemoteData GetRemoteData(Artist local, List remote) + private Author GetSkyhookData(string foreignId) { - var result = new RemoteData(); try { - result.Entity = _artistInfo.GetArtistInfo(local.Metadata.Value.ForeignArtistId, local.MetadataProfileId); - result.Metadata = new List { result.Entity.Metadata.Value }; + return _artistInfo.GetAuthorInfo(foreignId); } catch (ArtistNotFoundException) { - _logger.Error($"Could not find artist with id {local.Metadata.Value.ForeignArtistId}"); + _logger.Error($"Could not find artist with id {foreignId}"); + } + + return null; + } + + protected override RemoteData GetRemoteData(Author local, List remote, Author data) + { + var result = new RemoteData(); + + if (data != null) + { + result.Entity = data; + result.Metadata = new List { data.Metadata.Value }; } return result; } - protected override bool ShouldDelete(Artist local) + protected override bool ShouldDelete(Author local) { return !_mediaFileService.GetFilesByArtist(local.Id).Any(); } - protected override void LogProgress(Artist local) + protected override void LogProgress(Author local) { _logger.ProgressInfo("Updating Info for {0}", local.Name); } - protected override bool IsMerge(Artist local, Artist remote) + protected override bool IsMerge(Author local, Author remote) { - return local.ArtistMetadataId != remote.Metadata.Value.Id; + _logger.Trace($"local: {local.AuthorMetadataId} remote: {remote.Metadata.Value.Id}"); + return local.AuthorMetadataId != remote.Metadata.Value.Id; } - protected override UpdateResult UpdateEntity(Artist local, Artist remote) + protected override UpdateResult UpdateEntity(Author local, Author remote) { - UpdateResult result = UpdateResult.None; + var result = UpdateResult.None; if (!local.Metadata.Value.Equals(remote.Metadata.Value)) { @@ -112,6 +130,7 @@ namespace NzbDrone.Core.Music local.UseMetadataFrom(remote); local.Metadata = remote.Metadata; + local.Series = remote.Series.Value; local.LastInfoSync = DateTime.UtcNow; try @@ -127,20 +146,20 @@ namespace NzbDrone.Core.Music return result; } - protected override UpdateResult MoveEntity(Artist local, Artist remote) + protected override UpdateResult MoveEntity(Author local, Author remote) { - _logger.Debug($"Updating MusicBrainz id for {local} to {remote}"); + _logger.Debug($"Updating foreign id for {local} to {remote}"); // We are moving from one metadata to another (will already have been poplated) - local.ArtistMetadataId = remote.Metadata.Value.Id; + local.AuthorMetadataId = remote.Metadata.Value.Id; local.Metadata = remote.Metadata.Value; // Update list exclusion if one exists - var importExclusion = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignArtistId); + var importExclusion = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignAuthorId); if (importExclusion != null) { - importExclusion.ForeignId = remote.Metadata.Value.ForeignArtistId; + importExclusion.ForeignId = remote.Metadata.Value.ForeignAuthorId; _importListExclusionService.Update(importExclusion); } @@ -151,121 +170,124 @@ namespace NzbDrone.Core.Music return UpdateResult.UpdateTags; } - protected override UpdateResult MergeEntity(Artist local, Artist target, Artist remote) + protected override UpdateResult MergeEntity(Author local, Author target, Author remote) { _logger.Warn($"Artist {local} was replaced with {remote} because the original was a duplicate."); // Update list exclusion if one exists - var importExclusionLocal = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignArtistId); + var importExclusionLocal = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignAuthorId); if (importExclusionLocal != null) { - var importExclusionTarget = _importListExclusionService.FindByForeignId(target.Metadata.Value.ForeignArtistId); + var importExclusionTarget = _importListExclusionService.FindByForeignId(target.Metadata.Value.ForeignAuthorId); if (importExclusionTarget == null) { - importExclusionLocal.ForeignId = remote.Metadata.Value.ForeignArtistId; + importExclusionLocal.ForeignId = remote.Metadata.Value.ForeignAuthorId; _importListExclusionService.Update(importExclusionLocal); } } // move any albums over to the new artist and remove the local artist var albums = _albumService.GetAlbumsByArtist(local.Id); - albums.ForEach(x => x.ArtistMetadataId = target.ArtistMetadataId); + albums.ForEach(x => x.AuthorMetadataId = target.AuthorMetadataId); _albumService.UpdateMany(albums); _artistService.DeleteArtist(local.Id, false); // Update history entries to new id var items = _historyService.GetByArtist(local.Id, null); - items.ForEach(x => x.ArtistId = target.Id); + items.ForEach(x => x.AuthorId = target.Id); _historyService.UpdateMany(items); // We know we need to update tags as artist id has changed return UpdateResult.UpdateTags; } - protected override Artist GetEntityByForeignId(Artist local) + protected override Author GetEntityByForeignId(Author local) { - return _artistService.FindById(local.ForeignArtistId); + return _artistService.FindById(local.ForeignAuthorId); } - protected override void SaveEntity(Artist local) + protected override void SaveEntity(Author local) { _artistService.UpdateArtist(local); } - protected override void DeleteEntity(Artist local, bool deleteFiles) + protected override void DeleteEntity(Author local, bool deleteFiles) { _artistService.DeleteArtist(local.Id, true); } - protected override List GetRemoteChildren(Artist remote) + protected override List GetRemoteChildren(Author local, Author remote) { - var all = remote.Albums.Value.DistinctBy(m => m.ForeignAlbumId).ToList(); - var ids = all.SelectMany(x => x.OldForeignAlbumIds.Concat(new List { x.ForeignAlbumId })).ToList(); + var filtered = _metadataProfileService.FilterBooks(remote, local.MetadataProfileId); + + var all = filtered.DistinctBy(m => m.ForeignBookId).ToList(); + var ids = all.Select(x => x.ForeignBookId).ToList(); var excluded = _importListExclusionService.FindByForeignId(ids).Select(x => x.ForeignId).ToList(); - return all.Where(x => !excluded.Contains(x.ForeignAlbumId) && !x.OldForeignAlbumIds.Any(y => excluded.Contains(y))).ToList(); + return all.Where(x => !excluded.Contains(x.ForeignBookId)).ToList(); } - protected override List GetLocalChildren(Artist entity, List remoteChildren) + protected override List GetLocalChildren(Author entity, List remoteChildren) { - return _albumService.GetAlbumsForRefresh(entity.ArtistMetadataId, - remoteChildren.Select(x => x.ForeignAlbumId) - .Concat(remoteChildren.SelectMany(x => x.OldForeignAlbumIds))); + return _albumService.GetAlbumsForRefresh(entity.AuthorMetadataId, + remoteChildren.Select(x => x.ForeignBookId)); } - protected override Tuple> GetMatchingExistingChildren(List existingChildren, Album remote) + protected override Tuple> GetMatchingExistingChildren(List existingChildren, Book remote) { - var existingChild = existingChildren.SingleOrDefault(x => x.ForeignAlbumId == remote.ForeignAlbumId); - var mergeChildren = existingChildren.Where(x => remote.OldForeignAlbumIds.Contains(x.ForeignAlbumId)).ToList(); + var existingChild = existingChildren.SingleOrDefault(x => x.ForeignBookId == remote.ForeignBookId); + var mergeChildren = new List(); return Tuple.Create(existingChild, mergeChildren); } - protected override void PrepareNewChild(Album child, Artist entity) + protected override void PrepareNewChild(Book child, Author entity) { - child.Artist = entity; - child.ArtistMetadata = entity.Metadata.Value; - child.ArtistMetadataId = entity.Metadata.Value.Id; + child.Author = entity; + child.AuthorMetadata = entity.Metadata.Value; + child.AuthorMetadataId = entity.Metadata.Value.Id; child.Added = DateTime.UtcNow; child.LastInfoSync = DateTime.MinValue; - child.ProfileId = entity.QualityProfileId; child.Monitored = entity.Monitored; } - protected override void PrepareExistingChild(Album local, Album remote, Artist entity) + protected override void PrepareExistingChild(Book local, Book remote, Author entity) { - local.Artist = entity; - local.ArtistMetadata = entity.Metadata.Value; - local.ArtistMetadataId = entity.Metadata.Value.Id; + local.Author = entity; + local.AuthorMetadata = entity.Metadata.Value; + local.AuthorMetadataId = entity.Metadata.Value.Id; + + remote.UseDbFieldsFrom(local); } - protected override void AddChildren(List children) + protected override void AddChildren(List children) { _albumService.InsertMany(children); } - protected override bool RefreshChildren(SortedChildren localChildren, List remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) + protected override bool RefreshChildren(SortedChildren localChildren, List remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) { - // we always want to end up refreshing the albums since we don't yet have proper data - Ensure.That(localChildren.UpToDate.Count, () => localChildren.UpToDate.Count).IsLessThanOrEqualTo(0); - return _refreshAlbumService.RefreshAlbumInfo(localChildren.All, remoteChildren, forceChildRefresh, forceUpdateFileTags, lastUpdate); + return _refreshAlbumService.RefreshAlbumInfo(localChildren.All, remoteChildren, remoteData, forceChildRefresh, forceUpdateFileTags, lastUpdate); } - protected override void PublishEntityUpdatedEvent(Artist entity) + protected override void PublishEntityUpdatedEvent(Author entity) { _eventAggregator.PublishEvent(new ArtistUpdatedEvent(entity)); } - protected override void PublishRefreshCompleteEvent(Artist entity) + protected override void PublishRefreshCompleteEvent(Author entity) { + // little hack - trigger the series update here + _refreshSeriesService.RefreshSeriesInfo(entity.AuthorMetadataId, entity.Series, entity, false, false, null); + _eventAggregator.PublishEvent(new ArtistRefreshCompleteEvent(entity)); } - protected override void PublishChildrenUpdatedEvent(Artist entity, List newChildren, List updateChildren) + protected override void PublishChildrenUpdatedEvent(Author entity, List newChildren, List updateChildren) { _eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(entity, newChildren, updateChildren)); } - private void Rescan(List artistIds, bool isNew, CommandTrigger trigger, bool infoUpdated) + private void Rescan(List authorIds, bool isNew, CommandTrigger trigger, bool infoUpdated) { var rescanAfterRefresh = _configService.RescanAfterRefresh; var shouldRescan = true; @@ -297,20 +319,21 @@ namespace NzbDrone.Core.Music // (but don't add new artists to reduce repeated searches against api) var folders = _rootFolderService.All().Select(x => x.Path).ToList(); - _commandQueueManager.Push(new RescanFoldersCommand(folders, FilterFilesType.Matched, false, artistIds)); + _commandQueueManager.Push(new RescanFoldersCommand(folders, FilterFilesType.Matched, false, authorIds)); } } - private void RefreshSelectedArtists(List artistIds, bool isNew, CommandTrigger trigger) + private void RefreshSelectedArtists(List authorIds, bool isNew, CommandTrigger trigger) { - bool updated = false; - var artists = _artistService.GetArtists(artistIds); + var updated = false; + var artists = _artistService.GetArtists(authorIds); foreach (var artist in artists) { try { - updated |= RefreshEntityInfo(artist, null, true, false, null); + var data = GetSkyhookData(artist.ForeignAuthorId); + updated |= RefreshEntityInfo(artist, null, data, true, false, null); } catch (Exception e) { @@ -318,12 +341,12 @@ namespace NzbDrone.Core.Music } } - Rescan(artistIds, isNew, trigger, updated); + Rescan(authorIds, isNew, trigger, updated); } public void Execute(BulkRefreshArtistCommand message) { - RefreshSelectedArtists(message.ArtistIds, message.AreNewArtists, message.Trigger); + RefreshSelectedArtists(message.AuthorIds, message.AreNewArtists, message.Trigger); } public void Execute(RefreshArtistCommand message) @@ -331,15 +354,15 @@ namespace NzbDrone.Core.Music var trigger = message.Trigger; var isNew = message.IsNewArtist; - if (message.ArtistId.HasValue) + if (message.AuthorId.HasValue) { - RefreshSelectedArtists(new List { message.ArtistId.Value }, isNew, trigger); + RefreshSelectedArtists(new List { message.AuthorId.Value }, isNew, trigger); } else { var updated = false; var artists = _artistService.GetAllArtists().OrderBy(c => c.Name).ToList(); - var artistIds = artists.Select(x => x.Id).ToList(); + var authorIds = artists.Select(x => x.Id).ToList(); var updatedMusicbrainzArtists = new HashSet(); @@ -353,12 +376,13 @@ namespace NzbDrone.Core.Music var manualTrigger = message.Trigger == CommandTrigger.Manual; if ((updatedMusicbrainzArtists == null && _checkIfArtistShouldBeRefreshed.ShouldRefresh(artist)) || - (updatedMusicbrainzArtists != null && updatedMusicbrainzArtists.Contains(artist.ForeignArtistId)) || + (updatedMusicbrainzArtists != null && updatedMusicbrainzArtists.Contains(artist.ForeignAuthorId)) || manualTrigger) { try { - updated |= RefreshEntityInfo(artist, null, manualTrigger, false, message.LastStartTime); + var data = GetSkyhookData(artist.ForeignAuthorId); + updated |= RefreshEntityInfo(artist, null, data, manualTrigger, false, message.LastStartTime); } catch (Exception e) { @@ -371,7 +395,7 @@ namespace NzbDrone.Core.Music } } - Rescan(artistIds, isNew, trigger, updated); + Rescan(authorIds, isNew, trigger, updated); } } } diff --git a/src/NzbDrone.Core/Books/Services/RefreshBookService.cs b/src/NzbDrone.Core/Books/Services/RefreshBookService.cs new file mode 100644 index 000000000..d2a5abd4e --- /dev/null +++ b/src/NzbDrone.Core/Books/Services/RefreshBookService.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.History; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Music.Events; + +namespace NzbDrone.Core.Music +{ + public interface IRefreshAlbumService + { + bool RefreshAlbumInfo(Book album, List remoteAlbums, Author remoteData, bool forceUpdateFileTags); + bool RefreshAlbumInfo(List albums, List remoteAlbums, Author remoteData, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate); + } + + public class RefreshAlbumService : RefreshEntityServiceBase, IRefreshAlbumService + { + private readonly IAlbumService _albumService; + private readonly IArtistService _artistService; + private readonly IAddArtistService _addArtistService; + private readonly IProvideBookInfo _albumInfo; + private readonly IMediaFileService _mediaFileService; + private readonly IHistoryService _historyService; + private readonly IEventAggregator _eventAggregator; + private readonly ICheckIfAlbumShouldBeRefreshed _checkIfAlbumShouldBeRefreshed; + private readonly IMapCoversToLocal _mediaCoverService; + private readonly Logger _logger; + + public RefreshAlbumService(IAlbumService albumService, + IArtistService artistService, + IAddArtistService addArtistService, + IArtistMetadataService artistMetadataService, + IProvideBookInfo albumInfo, + IMediaFileService mediaFileService, + IHistoryService historyService, + IEventAggregator eventAggregator, + ICheckIfAlbumShouldBeRefreshed checkIfAlbumShouldBeRefreshed, + IMapCoversToLocal mediaCoverService, + Logger logger) + : base(logger, artistMetadataService) + { + _albumService = albumService; + _artistService = artistService; + _addArtistService = addArtistService; + _albumInfo = albumInfo; + _mediaFileService = mediaFileService; + _historyService = historyService; + _eventAggregator = eventAggregator; + _checkIfAlbumShouldBeRefreshed = checkIfAlbumShouldBeRefreshed; + _mediaCoverService = mediaCoverService; + _logger = logger; + } + + protected override RemoteData GetRemoteData(Book local, List remote, Author data) + { + var result = new RemoteData(); + + var book = remote.SingleOrDefault(x => x.ForeignWorkId == local.ForeignWorkId); + + if (book == null && ShouldDelete(local)) + { + return result; + } + + if (book == null) + { + book = data.Books.Value.SingleOrDefault(x => x.ForeignWorkId == local.ForeignWorkId); + } + + result.Entity = book; + if (result.Entity != null) + { + result.Entity.Id = local.Id; + } + + return result; + } + + protected override void EnsureNewParent(Book local, Book remote) + { + // Make sure the appropriate artist exists (it could be that an album changes parent) + // The artistMetadata entry will be in the db but make sure a corresponding artist is too + // so that the album doesn't just disappear. + + // TODO filter by metadata id before hitting database + _logger.Trace($"Ensuring parent artist exists [{remote.AuthorMetadata.Value.ForeignAuthorId}]"); + + var newArtist = _artistService.FindById(remote.AuthorMetadata.Value.ForeignAuthorId); + + if (newArtist == null) + { + var oldArtist = local.Author.Value; + var addArtist = new Author + { + Metadata = remote.AuthorMetadata.Value, + MetadataProfileId = oldArtist.MetadataProfileId, + QualityProfileId = oldArtist.QualityProfileId, + RootFolderPath = oldArtist.RootFolderPath, + Monitored = oldArtist.Monitored, + Tags = oldArtist.Tags + }; + _logger.Debug($"Adding missing parent artist {addArtist}"); + _addArtistService.AddArtist(addArtist); + } + } + + protected override bool ShouldDelete(Book local) + { + // not manually added and has no files + return local.AddOptions.AddType != AlbumAddType.Manual && + !_mediaFileService.GetFilesByAlbum(local.Id).Any(); + } + + protected override void LogProgress(Book local) + { + _logger.ProgressInfo("Updating Info for {0}", local.Title); + } + + protected override bool IsMerge(Book local, Book remote) + { + return local.ForeignBookId != remote.ForeignBookId; + } + + protected override UpdateResult UpdateEntity(Book local, Book remote) + { + UpdateResult result; + + remote.UseDbFieldsFrom(local); + + if (local.Title != (remote.Title ?? "Unknown") || + local.ForeignBookId != remote.ForeignBookId || + local.AuthorMetadata.Value.ForeignAuthorId != remote.AuthorMetadata.Value.ForeignAuthorId) + { + result = UpdateResult.UpdateTags; + } + else if (!local.Equals(remote)) + { + result = UpdateResult.Standard; + } + else + { + result = UpdateResult.None; + } + + // Force update and fetch covers if images have changed so that we can write them into tags + // if (remote.Images.Any() && !local.Images.SequenceEqual(remote.Images)) + // { + // _mediaCoverService.EnsureAlbumCovers(remote); + // result = UpdateResult.UpdateTags; + // } + local.UseMetadataFrom(remote); + + local.AuthorMetadataId = remote.AuthorMetadata.Value.Id; + local.LastInfoSync = DateTime.UtcNow; + + return result; + } + + protected override UpdateResult MergeEntity(Book local, Book target, Book remote) + { + _logger.Warn($"Album {local} was merged with {remote} because the original was a duplicate."); + + // Update album ids for trackfiles + var files = _mediaFileService.GetFilesByAlbum(local.Id); + files.ForEach(x => x.BookId = target.Id); + _mediaFileService.Update(files); + + // Update album ids for history + var items = _historyService.GetByAlbum(local.Id, null); + items.ForEach(x => x.BookId = target.Id); + _historyService.UpdateMany(items); + + // Finally delete the old album + _albumService.DeleteMany(new List { local }); + + return UpdateResult.UpdateTags; + } + + protected override Book GetEntityByForeignId(Book local) + { + return _albumService.FindById(local.ForeignBookId); + } + + protected override void SaveEntity(Book local) + { + // Use UpdateMany to avoid firing the album edited event + _albumService.UpdateMany(new List { local }); + } + + protected override void DeleteEntity(Book local, bool deleteFiles) + { + _albumService.DeleteAlbum(local.Id, true); + } + + protected override List GetRemoteChildren(Book local, Book remote) + { + return new List(); + } + + protected override List GetLocalChildren(Book entity, List remoteChildren) + { + return new List(); + } + + protected override Tuple> GetMatchingExistingChildren(List existingChildren, object remote) + { + return null; + } + + protected override void PrepareNewChild(object child, Book entity) + { + } + + protected override void PrepareExistingChild(object local, object remote, Book entity) + { + } + + protected override void AddChildren(List children) + { + } + + protected override bool RefreshChildren(SortedChildren localChildren, List remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) + { + return false; + } + + protected override void PublishEntityUpdatedEvent(Book entity) + { + // Fetch fresh from DB so all lazy loads are available + _eventAggregator.PublishEvent(new AlbumUpdatedEvent(_albumService.GetAlbum(entity.Id))); + } + + public bool RefreshAlbumInfo(List albums, List remoteAlbums, Author remoteData, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) + { + var updated = false; + + HashSet updatedMusicbrainzAlbums = null; + + if (lastUpdate.HasValue && lastUpdate.Value.AddDays(14) > DateTime.UtcNow) + { + updatedMusicbrainzAlbums = _albumInfo.GetChangedAlbums(lastUpdate.Value); + } + + foreach (var album in albums) + { + if (forceAlbumRefresh || + (updatedMusicbrainzAlbums == null && _checkIfAlbumShouldBeRefreshed.ShouldRefresh(album)) || + (updatedMusicbrainzAlbums != null && updatedMusicbrainzAlbums.Contains(album.ForeignBookId))) + { + updated |= RefreshAlbumInfo(album, remoteAlbums, remoteData, forceUpdateFileTags); + } + else + { + _logger.Debug("Skipping refresh of album: {0}", album.Title); + } + } + + return updated; + } + + public bool RefreshAlbumInfo(Book album, List remoteAlbums, Author remoteData, bool forceUpdateFileTags) + { + return RefreshEntityInfo(album, remoteAlbums, remoteData, true, forceUpdateFileTags, null); + } + } +} diff --git a/src/NzbDrone.Core/Music/Services/RefreshEntityServiceBase.cs b/src/NzbDrone.Core/Books/Services/RefreshEntityServiceBase.cs similarity index 84% rename from src/NzbDrone.Core/Music/Services/RefreshEntityServiceBase.cs rename to src/NzbDrone.Core/Books/Services/RefreshEntityServiceBase.cs index 34a9a0936..4aaae5911 100644 --- a/src/NzbDrone.Core/Music/Services/RefreshEntityServiceBase.cs +++ b/src/NzbDrone.Core/Books/Services/RefreshEntityServiceBase.cs @@ -50,14 +50,14 @@ namespace NzbDrone.Core.Music public class RemoteData { public TEntity Entity { get; set; } - public List Metadata { get; set; } + public List Metadata { get; set; } } protected virtual void LogProgress(TEntity local) { } - protected abstract RemoteData GetRemoteData(TEntity local, List remote); + protected abstract RemoteData GetRemoteData(TEntity local, List remote, Author data); protected virtual void EnsureNewParent(TEntity local, TEntity remote) { @@ -87,14 +87,14 @@ namespace NzbDrone.Core.Music protected abstract void SaveEntity(TEntity local); protected abstract void DeleteEntity(TEntity local, bool deleteFiles); - protected abstract List GetRemoteChildren(TEntity remote); + protected abstract List GetRemoteChildren(TEntity local, TEntity remote); protected abstract List GetLocalChildren(TEntity entity, List remoteChildren); protected abstract Tuple> GetMatchingExistingChildren(List existingChildren, TChild remote); protected abstract void PrepareNewChild(TChild child, TEntity entity); protected abstract void PrepareExistingChild(TChild local, TChild remote, TEntity entity); protected abstract void AddChildren(List children); - protected abstract bool RefreshChildren(SortedChildren localChildren, List remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate); + protected abstract bool RefreshChildren(SortedChildren localChildren, List remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate); protected virtual void PublishEntityUpdatedEvent(TEntity entity) { @@ -108,13 +108,13 @@ namespace NzbDrone.Core.Music { } - public bool RefreshEntityInfo(TEntity local, List remoteList, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) + public bool RefreshEntityInfo(TEntity local, List remoteItems, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) { - bool updated = false; + var updated = false; LogProgress(local); - var data = GetRemoteData(local, remoteList); + var data = GetRemoteData(local, remoteItems, remoteData); var remote = data.Entity; if (remote == null) @@ -176,8 +176,8 @@ namespace NzbDrone.Core.Music _logger.Trace($"updated: {updated} forceUpdateFileTags: {forceUpdateFileTags}"); - var remoteChildren = GetRemoteChildren(remote); - updated |= SortChildren(local, remoteChildren, forceChildRefresh, forceUpdateFileTags, lastUpdate); + var remoteChildren = GetRemoteChildren(local, remote); + updated |= SortChildren(local, remoteChildren, remoteData, forceChildRefresh, forceUpdateFileTags, lastUpdate); // Do this last so entity only marked as refreshed if refresh of children completed successfully _logger.Trace($"Saving {typeof(TEntity).Name} {local}"); @@ -195,25 +195,25 @@ namespace NzbDrone.Core.Music return updated; } - public bool RefreshEntityInfo(List localList, List remoteList, bool forceChildRefresh, bool forceUpdateFileTags) + public bool RefreshEntityInfo(List localList, List remoteItems, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags) { - bool updated = false; + var updated = false; foreach (var entity in localList) { - updated |= RefreshEntityInfo(entity, remoteList, forceChildRefresh, forceUpdateFileTags, null); + updated |= RefreshEntityInfo(entity, remoteItems, remoteData, forceChildRefresh, forceUpdateFileTags, null); } return updated; } - public UpdateResult UpdateArtistMetadata(List data) + public UpdateResult UpdateArtistMetadata(List data) { - var remoteMetadata = data.DistinctBy(x => x.ForeignArtistId).ToList(); + var remoteMetadata = data.DistinctBy(x => x.ForeignAuthorId).ToList(); var updated = _artistMetadataService.UpsertMany(remoteMetadata); return updated ? UpdateResult.UpdateTags : UpdateResult.None; } - protected bool SortChildren(TEntity entity, List remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) + protected bool SortChildren(TEntity entity, List remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) { // Get existing children (and children to be) from the database var localChildren = GetLocalChildren(entity, remoteChildren); @@ -265,20 +265,23 @@ namespace NzbDrone.Core.Music } } - _logger.Debug("{0} {1} {2}s up to date. Adding {3}, Updating {4}, Merging {5}, Deleting {6}.", - entity, - sortedChildren.UpToDate.Count, - typeof(TChild).Name.ToLower(), - sortedChildren.Added.Count, - sortedChildren.Updated.Count, - sortedChildren.Merged.Count, - sortedChildren.Deleted.Count); + if (typeof(TChild) != typeof(object)) + { + _logger.Debug("{0} {1} {2}s up to date. Adding {3}, Updating {4}, Merging {5}, Deleting {6}.", + entity, + sortedChildren.UpToDate.Count, + typeof(TChild).Name.ToLower(), + sortedChildren.Added.Count, + sortedChildren.Updated.Count, + sortedChildren.Merged.Count, + sortedChildren.Deleted.Count); + } // Add in the new children (we have checked that foreign IDs don't clash) AddChildren(sortedChildren.Added); // now trigger updates - var updated = RefreshChildren(sortedChildren, remoteChildren, forceChildRefresh, forceUpdateFileTags, lastUpdate); + var updated = RefreshChildren(sortedChildren, remoteChildren, remoteData, forceChildRefresh, forceUpdateFileTags, lastUpdate); PublishChildrenUpdatedEvent(entity, sortedChildren.Added, sortedChildren.Updated); return updated; diff --git a/src/NzbDrone.Core/Books/Services/RefreshSeriesBookLinkService.cs b/src/NzbDrone.Core/Books/Services/RefreshSeriesBookLinkService.cs new file mode 100644 index 000000000..8c7ab5710 --- /dev/null +++ b/src/NzbDrone.Core/Books/Services/RefreshSeriesBookLinkService.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; + +namespace NzbDrone.Core.Music +{ + public interface IRefreshSeriesBookLinkService + { + bool RefreshSeriesBookLinkInfo(List add, List update, List> merge, List delete, List upToDate, List remoteSeriesBookLinks, bool forceUpdateFileTags); + } + + public class RefreshSeriesBookLinkService : IRefreshSeriesBookLinkService + { + private readonly ISeriesBookLinkService _seriesBookLinkService; + private readonly Logger _logger; + + public RefreshSeriesBookLinkService(ISeriesBookLinkService trackService, + Logger logger) + { + _seriesBookLinkService = trackService; + _logger = logger; + } + + public bool RefreshSeriesBookLinkInfo(List add, List update, List> merge, List delete, List upToDate, List remoteSeriesBookLinks, bool forceUpdateFileTags) + { + var updateList = new List(); + + foreach (var link in update) + { + var remoteSeriesBookLink = remoteSeriesBookLinks.Single(e => e.Book.Value.Id == link.BookId); + link.UseMetadataFrom(remoteSeriesBookLink); + + // make sure title is not null + updateList.Add(link); + } + + _seriesBookLinkService.DeleteMany(delete); + _seriesBookLinkService.UpdateMany(updateList); + + return add.Any() || delete.Any() || updateList.Any() || merge.Any(); + } + } +} diff --git a/src/NzbDrone.Core/Books/Services/RefreshSeriesService.cs b/src/NzbDrone.Core/Books/Services/RefreshSeriesService.cs new file mode 100644 index 000000000..343fa2c74 --- /dev/null +++ b/src/NzbDrone.Core/Books/Services/RefreshSeriesService.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.Core.Music +{ + public interface IRefreshSeriesService + { + bool RefreshSeriesInfo(int authorMetadataId, List remoteAlbums, Author remoteData, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate); + } + + public class RefreshSeriesService : RefreshEntityServiceBase, IRefreshSeriesService + { + private readonly IAlbumService _bookService; + private readonly ISeriesService _seriesService; + private readonly ISeriesBookLinkService _linkService; + private readonly IRefreshSeriesBookLinkService _refreshLinkService; + private readonly Logger _logger; + + public RefreshSeriesService(IAlbumService bookService, + ISeriesService seriesService, + ISeriesBookLinkService linkService, + IRefreshSeriesBookLinkService refreshLinkService, + IArtistMetadataService artistMetadataService, + Logger logger) + : base(logger, artistMetadataService) + { + _bookService = bookService; + _seriesService = seriesService; + _linkService = linkService; + _refreshLinkService = refreshLinkService; + _logger = logger; + } + + protected override RemoteData GetRemoteData(Series local, List remote, Author data) + { + return new RemoteData + { + Entity = remote.SingleOrDefault(x => x.ForeignSeriesId == local.ForeignSeriesId) + }; + } + + protected override bool IsMerge(Series local, Series remote) + { + return local.ForeignSeriesId != remote.ForeignSeriesId; + } + + protected override UpdateResult UpdateEntity(Series local, Series remote) + { + if (local.Equals(remote)) + { + return UpdateResult.None; + } + + local.UseMetadataFrom(remote); + + return UpdateResult.UpdateTags; + } + + protected override Series GetEntityByForeignId(Series local) + { + return _seriesService.FindById(local.ForeignSeriesId); + } + + protected override void SaveEntity(Series local) + { + // Use UpdateMany to avoid firing the album edited event + _seriesService.UpdateMany(new List { local }); + } + + protected override void DeleteEntity(Series local, bool deleteFiles) + { + _seriesService.Delete(local.Id); + } + + protected override List GetRemoteChildren(Series local, Series remote) + { + return remote.LinkItems; + } + + protected override List GetLocalChildren(Series entity, List remoteChildren) + { + return _linkService.GetLinksBySeries(entity.Id); + } + + protected override Tuple> GetMatchingExistingChildren(List existingChildren, SeriesBookLink remote) + { + var existingChild = existingChildren.SingleOrDefault(x => x.BookId == remote.Book.Value.Id); + var mergeChildren = new List(); + return Tuple.Create(existingChild, mergeChildren); + } + + protected override void PrepareNewChild(SeriesBookLink child, Series entity) + { + child.Series = entity; + child.SeriesId = entity.Id; + child.BookId = child.Book.Value.Id; + } + + protected override void PrepareExistingChild(SeriesBookLink local, SeriesBookLink remote, Series entity) + { + local.Series = entity; + local.SeriesId = entity.Id; + + remote.Id = local.Id; + remote.BookId = local.BookId; + remote.SeriesId = entity.Id; + } + + protected override void AddChildren(List children) + { + _linkService.InsertMany(children); + } + + protected override bool RefreshChildren(SortedChildren localChildren, List remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) + { + return _refreshLinkService.RefreshSeriesBookLinkInfo(localChildren.Added, localChildren.Updated, localChildren.Merged, localChildren.Deleted, localChildren.UpToDate, remoteChildren, forceUpdateFileTags); + } + + public bool RefreshSeriesInfo(int authorMetadataId, List remoteSeries, Author remoteData, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) + { + var updated = false; + + var existingByAuthor = _seriesService.GetByAuthorMetadataId(authorMetadataId); + var existingBySeries = _seriesService.FindById(remoteSeries.Select(x => x.ForeignSeriesId)); + var existing = existingByAuthor.Concat(existingBySeries).GroupBy(x => x.ForeignSeriesId).Select(x => x.First()).ToList(); + + var books = _bookService.GetAlbumsByArtistMetadataId(authorMetadataId); + var bookDict = books.ToDictionary(x => x.ForeignWorkId); + var links = new List(); + + foreach (var s in remoteData.Series.Value) + { + s.LinkItems.Value.ForEach(x => x.Series = s); + links.AddRange(s.LinkItems.Value.Where(x => bookDict.ContainsKey(x.Book.Value.ForeignWorkId))); + } + + var grouped = links.GroupBy(x => x.Series.Value); + + // Put in the links that go with the books we actually have + foreach (var group in grouped) + { + group.Key.LinkItems = group.ToList(); + } + + remoteSeries = grouped.Select(x => x.Key).ToList(); + + var toAdd = remoteSeries.ExceptBy(x => x.ForeignSeriesId, existing, x => x.ForeignSeriesId, StringComparer.Ordinal).ToList(); + var all = toAdd.Union(existing).ToList(); + + _seriesService.InsertMany(toAdd); + + foreach (var item in all) + { + updated |= RefreshEntityInfo(item, remoteSeries, remoteData, true, forceUpdateFileTags, null); + } + + return updated; + } + } +} diff --git a/src/NzbDrone.Core/Books/Services/SeriesBookLinkService.cs b/src/NzbDrone.Core/Books/Services/SeriesBookLinkService.cs new file mode 100644 index 000000000..bf08a51d2 --- /dev/null +++ b/src/NzbDrone.Core/Books/Services/SeriesBookLinkService.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Music +{ + public interface ISeriesBookLinkService + { + List GetLinksBySeries(int seriesId); + void InsertMany(List model); + void UpdateMany(List model); + void DeleteMany(List model); + } + + public class SeriesBookLinkService : ISeriesBookLinkService + { + private readonly ISeriesBookLinkRepository _repo; + + public SeriesBookLinkService(ISeriesBookLinkRepository repo) + { + _repo = repo; + } + + public List GetLinksBySeries(int seriesId) + { + return _repo.GetLinksBySeries(seriesId); + } + + public void InsertMany(List model) + { + _repo.InsertMany(model); + } + + public void UpdateMany(List model) + { + _repo.UpdateMany(model); + } + + public void DeleteMany(List model) + { + _repo.DeleteMany(model); + } + } +} diff --git a/src/NzbDrone.Core/Books/Services/SeriesService.cs b/src/NzbDrone.Core/Books/Services/SeriesService.cs new file mode 100644 index 000000000..018112240 --- /dev/null +++ b/src/NzbDrone.Core/Books/Services/SeriesService.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Music +{ + public interface ISeriesService + { + Series FindById(string foreignSeriesId); + List FindById(IEnumerable foreignSeriesId); + List GetByAuthorMetadataId(int authorMetadataId); + List GetByAuthorId(int authorId); + void Delete(int seriesId); + void InsertMany(IList series); + void UpdateMany(IList series); + } + + public class SeriesService : ISeriesService + { + private readonly ISeriesRepository _seriesRepository; + + public SeriesService(ISeriesRepository seriesRepository) + { + _seriesRepository = seriesRepository; + } + + public Series FindById(string foreignSeriesId) + { + return _seriesRepository.FindById(foreignSeriesId); + } + + public List FindById(IEnumerable foreignSeriesId) + { + return _seriesRepository.FindById(foreignSeriesId); + } + + public List GetByAuthorMetadataId(int authorMetadataId) + { + return _seriesRepository.GetByAuthorMetadataId(authorMetadataId); + } + + public List GetByAuthorId(int authorId) + { + return _seriesRepository.GetByAuthorId(authorId); + } + + public void Delete(int seriesId) + { + _seriesRepository.Delete(seriesId); + } + + public void InsertMany(IList series) + { + _seriesRepository.InsertMany(series); + } + + public void UpdateMany(IList series) + { + _seriesRepository.UpdateMany(series); + } + } +} diff --git a/src/NzbDrone.Core/Music/Utilities/AddArtistValidator.cs b/src/NzbDrone.Core/Books/Utilities/AddArtistValidator.cs similarity index 91% rename from src/NzbDrone.Core/Music/Utilities/AddArtistValidator.cs rename to src/NzbDrone.Core/Books/Utilities/AddArtistValidator.cs index c77cfacc2..11aab0893 100644 --- a/src/NzbDrone.Core/Music/Utilities/AddArtistValidator.cs +++ b/src/NzbDrone.Core/Books/Utilities/AddArtistValidator.cs @@ -7,10 +7,10 @@ namespace NzbDrone.Core.Music { public interface IAddArtistValidator { - ValidationResult Validate(Artist instance); + ValidationResult Validate(Author instance); } - public class AddArtistValidator : AbstractValidator, IAddArtistValidator + public class AddArtistValidator : AbstractValidator, IAddArtistValidator { public AddArtistValidator(RootFolderValidator rootFolderValidator, ArtistPathValidator artistPathValidator, diff --git a/src/NzbDrone.Core/Music/Utilities/ArtistPathBuilder.cs b/src/NzbDrone.Core/Books/Utilities/ArtistPathBuilder.cs similarity index 87% rename from src/NzbDrone.Core/Music/Utilities/ArtistPathBuilder.cs rename to src/NzbDrone.Core/Books/Utilities/ArtistPathBuilder.cs index 0f84b6de7..f0667bca7 100644 --- a/src/NzbDrone.Core/Music/Utilities/ArtistPathBuilder.cs +++ b/src/NzbDrone.Core/Books/Utilities/ArtistPathBuilder.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Music { public interface IBuildArtistPaths { - string BuildPath(Artist artist, bool useExistingRelativeFolder); + string BuildPath(Author artist, bool useExistingRelativeFolder); } public class ArtistPathBuilder : IBuildArtistPaths @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Music _rootFolderService = rootFolderService; } - public string BuildPath(Artist artist, bool useExistingRelativeFolder) + public string BuildPath(Author artist, bool useExistingRelativeFolder) { if (artist.RootFolderPath.IsNullOrWhiteSpace()) { @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Music return Path.Combine(artist.RootFolderPath, _fileNameBuilder.GetArtistFolder(artist)); } - private string GetExistingRelativePath(Artist artist) + private string GetExistingRelativePath(Author artist) { var rootFolderPath = _rootFolderService.GetBestRootFolderPath(artist.Path); diff --git a/src/NzbDrone.Core/Music/Utilities/ShouldRefreshAlbum.cs b/src/NzbDrone.Core/Books/Utilities/ShouldRefreshAlbum.cs similarity index 93% rename from src/NzbDrone.Core/Music/Utilities/ShouldRefreshAlbum.cs rename to src/NzbDrone.Core/Books/Utilities/ShouldRefreshAlbum.cs index 5dac303ce..6c56b2690 100644 --- a/src/NzbDrone.Core/Music/Utilities/ShouldRefreshAlbum.cs +++ b/src/NzbDrone.Core/Books/Utilities/ShouldRefreshAlbum.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Core.Music { public interface ICheckIfAlbumShouldBeRefreshed { - bool ShouldRefresh(Album album); + bool ShouldRefresh(Book album); } public class ShouldRefreshAlbum : ICheckIfAlbumShouldBeRefreshed @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Music _logger = logger; } - public bool ShouldRefresh(Album album) + public bool ShouldRefresh(Book album) { if (album.LastInfoSync < DateTime.UtcNow.AddDays(-60)) { diff --git a/src/NzbDrone.Core/Music/Utilities/ShouldRefreshArtist.cs b/src/NzbDrone.Core/Books/Utilities/ShouldRefreshArtist.cs similarity index 95% rename from src/NzbDrone.Core/Music/Utilities/ShouldRefreshArtist.cs rename to src/NzbDrone.Core/Books/Utilities/ShouldRefreshArtist.cs index dc9fca5c7..0ddd01be7 100644 --- a/src/NzbDrone.Core/Music/Utilities/ShouldRefreshArtist.cs +++ b/src/NzbDrone.Core/Books/Utilities/ShouldRefreshArtist.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.Music { public interface ICheckIfArtistShouldBeRefreshed { - bool ShouldRefresh(Artist artist); + bool ShouldRefresh(Author artist); } public class ShouldRefreshArtist : ICheckIfArtistShouldBeRefreshed @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Music _logger = logger; } - public bool ShouldRefresh(Artist artist) + public bool ShouldRefresh(Author artist) { if (artist.LastInfoSync < DateTime.UtcNow.AddDays(-30)) { diff --git a/src/NzbDrone.Core/Books/output.txt b/src/NzbDrone.Core/Books/output.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index d0defc1b1..81f87f4d9 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -70,6 +70,8 @@ namespace NzbDrone.Core.Datastore protected virtual List Query(SqlBuilder builder) => _database.Query(builder).ToList(); + protected virtual List QueryDistinct(SqlBuilder builder) => _database.QueryDistinct(builder).ToList(); + protected List Query(Expression> where) => Query(Builder().Where(where)); public int Count() @@ -372,7 +374,11 @@ namespace NzbDrone.Core.Datastore { var sql = propertiesToUpdate == _properties ? _updateSql : GetUpdateSql(propertiesToUpdate); - // SqlBuilderExtensions.LogQuery(sql, models); + foreach (var model in models) + { + SqlBuilderExtensions.LogQuery(sql, model); + } + connection.Execute(sql, models, transaction: transaction); } diff --git a/src/NzbDrone.Core/Datastore/Converters/PrimaryAlbumTypeIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/PrimaryAlbumTypeIntConverter.cs deleted file mode 100644 index fde20de8a..000000000 --- a/src/NzbDrone.Core/Datastore/Converters/PrimaryAlbumTypeIntConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Datastore.Converters -{ - public class PrimaryAlbumTypeIntConverter : JsonConverter - { - public override PrimaryAlbumType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var item = reader.GetInt32(); - return (PrimaryAlbumType)item; - } - - public override void Write(Utf8JsonWriter writer, PrimaryAlbumType value, JsonSerializerOptions options) - { - writer.WriteNumberValue((int)value); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Converters/ReleaseStatusIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/ReleaseStatusIntConverter.cs deleted file mode 100644 index 68faafb85..000000000 --- a/src/NzbDrone.Core/Datastore/Converters/ReleaseStatusIntConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Datastore.Converters -{ - public class ReleaseStatusIntConverter : JsonConverter - { - public override ReleaseStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var item = reader.GetInt32(); - return (ReleaseStatus)item; - } - - public override void Write(Utf8JsonWriter writer, ReleaseStatus value, JsonSerializerOptions options) - { - writer.WriteNumberValue((int)value); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Converters/SecondaryAlbumTypeIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/SecondaryAlbumTypeIntConverter.cs deleted file mode 100644 index 7811327a1..000000000 --- a/src/NzbDrone.Core/Datastore/Converters/SecondaryAlbumTypeIntConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Datastore.Converters -{ - public class SecondaryAlbumTypeIntConverter : JsonConverter - { - public override SecondaryAlbumType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var item = reader.GetInt32(); - return (SecondaryAlbumType)item; - } - - public override void Write(Utf8JsonWriter writer, SecondaryAlbumType value, JsonSerializerOptions options) - { - writer.WriteNumberValue((int)value); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs index 061b2a73d..c5e3d3253 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs @@ -23,6 +23,11 @@ namespace NzbDrone.Core.Datastore return builder.Select(types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", ")); } + public static SqlBuilder SelectDistinct(this SqlBuilder builder, params Type[] types) + { + return builder.Select("DISTINCT " + types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", ")); + } + public static SqlBuilder SelectCount(this SqlBuilder builder) { return builder.Select("COUNT(*)"); diff --git a/src/NzbDrone.Core/Datastore/Extensions/SqlMapperExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/SqlMapperExtensions.cs index b4e715daa..222cbd9ad 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/SqlMapperExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/SqlMapperExtensions.cs @@ -110,6 +110,14 @@ namespace NzbDrone.Core.Datastore return db.Query(sql.RawSql, sql.Parameters); } + public static IEnumerable QueryDistinct(this IDatabase db, SqlBuilder builder) + { + var type = typeof(T); + var sql = builder.SelectDistinct(type).AddSelectTemplate(type); + + return db.Query(sql.RawSql, sql.Parameters); + } + public static IEnumerable QueryJoined(this IDatabase db, SqlBuilder builder, Func mapper) { var type = typeof(T); diff --git a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs index 3d05d5a0a..15a15b95f 100644 --- a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs +++ b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using FluentMigrator; using NzbDrone.Core.Datastore.Migration.Framework; @@ -19,13 +20,14 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("DefaultMetadataProfileId").AsInt32().WithDefaultValue(0) .WithColumn("DefaultQualityProfileId").AsInt32().WithDefaultValue(0) .WithColumn("DefaultMonitorOption").AsInt32().WithDefaultValue(0) - .WithColumn("DefaultTags").AsString().Nullable(); + .WithColumn("DefaultTags").AsString().Nullable() + .WithColumn("IsCalibreLibrary").AsBoolean() + .WithColumn("CalibreSettings").AsString().Nullable(); - Create.TableForModel("Artists") + Create.TableForModel("Authors") .WithColumn("CleanName").AsString().Indexed() .WithColumn("Path").AsString().Indexed() .WithColumn("Monitored").AsBoolean() - .WithColumn("AlbumFolder").AsBoolean() .WithColumn("LastInfoSync").AsDateTime().Nullable() .WithColumn("SortName").AsString().Nullable() .WithColumn("QualityProfileId").AsInt32().Nullable() @@ -33,10 +35,26 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Added").AsDateTime().Nullable() .WithColumn("AddOptions").AsString().Nullable() .WithColumn("MetadataProfileId").AsInt32().WithDefaultValue(1) - .WithColumn("ArtistMetadataId").AsInt32().Unique(); + .WithColumn("AuthorMetadataId").AsInt32().Unique(); - Create.TableForModel("ArtistMetadata") - .WithColumn("ForeignArtistId").AsString().Unique() + Create.TableForModel("Series") + .WithColumn("ForeignSeriesId").AsString().Unique() + .WithColumn("Title").AsString() + .WithColumn("Description").AsString().Nullable() + .WithColumn("Numbered").AsBoolean() + .WithColumn("WorkCount").AsInt32() + .WithColumn("PrimaryWorkCount").AsInt32(); + + Create.TableForModel("SeriesBookLink") + .WithColumn("SeriesId").AsInt32().Indexed().ForeignKey("Series", "Id").OnDelete(Rule.Cascade) + .WithColumn("BookId").AsInt32().ForeignKey("Books", "Id").OnDelete(Rule.Cascade) + .WithColumn("Position").AsString().Nullable() + .WithColumn("IsPrimary").AsBoolean(); + + Create.TableForModel("AuthorMetadata") + .WithColumn("ForeignAuthorId").AsString().Unique() + .WithColumn("GoodreadsId").AsInt32() + .WithColumn("TitleSlug").AsString().Unique() .WithColumn("Name").AsString() .WithColumn("Overview").AsString().Nullable() .WithColumn("Disambiguation").AsString().Nullable() @@ -46,69 +64,36 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Links").AsString().Nullable() .WithColumn("Genres").AsString().Nullable() .WithColumn("Ratings").AsString().Nullable() - .WithColumn("Members").AsString().Nullable() - .WithColumn("Aliases").AsString().WithDefaultValue("[]") - .WithColumn("OldForeignArtistIds").AsString().WithDefaultValue("[]"); - - Create.TableForModel("Albums") - .WithColumn("ForeignAlbumId").AsString().Unique() + .WithColumn("Aliases").AsString().WithDefaultValue("[]"); + + Create.TableForModel("Books") + .WithColumn("AuthorMetadataId").AsInt32().WithDefaultValue(0) + .WithColumn("ForeignBookId").AsString().Unique() + .WithColumn("ForeignWorkId").AsString().Indexed() + .WithColumn("GoodreadsId").AsInt32() + .WithColumn("TitleSlug").AsString().Unique() + .WithColumn("Isbn13").AsString().Nullable() + .WithColumn("Asin").AsString().Nullable() .WithColumn("Title").AsString() - .WithColumn("CleanTitle").AsString().Indexed() + .WithColumn("Language").AsString().Nullable() .WithColumn("Overview").AsString().Nullable() + .WithColumn("PageCount").AsInt32().Nullable() + .WithColumn("Disambiguation").AsString().Nullable() + .WithColumn("Publisher").AsString().Nullable() + .WithColumn("ReleaseDate").AsDateTime().Nullable() .WithColumn("Images").AsString() + .WithColumn("Links").AsString().Nullable() + .WithColumn("Genres").AsString().Nullable() + .WithColumn("Ratings").AsString().Nullable() + .WithColumn("CleanTitle").AsString().Indexed() .WithColumn("Monitored").AsBoolean() .WithColumn("LastInfoSync").AsDateTime().Nullable() - .WithColumn("ReleaseDate").AsDateTime().Nullable() - .WithColumn("Ratings").AsString().Nullable() - .WithColumn("Genres").AsString().Nullable() - .WithColumn("ProfileId").AsInt32().Nullable() .WithColumn("Added").AsDateTime().Nullable() - .WithColumn("AlbumType").AsString() - .WithColumn("AddOptions").AsString().Nullable() - .WithColumn("SecondaryTypes").AsString().Nullable() - .WithColumn("Disambiguation").AsString().Nullable() - .WithColumn("ArtistMetadataId").AsInt32().WithDefaultValue(0) - .WithColumn("AnyReleaseOk").AsBoolean().WithDefaultValue(true) - .WithColumn("Links").AsString().Nullable() - .WithColumn("OldForeignAlbumIds").AsString().WithDefaultValue("[]"); + .WithColumn("AddOptions").AsString().Nullable(); - Create.TableForModel("AlbumReleases") - .WithColumn("ForeignReleaseId").AsString().Unique() - .WithColumn("AlbumId").AsInt32().Indexed() - .WithColumn("Title").AsString() - .WithColumn("Status").AsString() - .WithColumn("Duration").AsInt32().WithDefaultValue(0) - .WithColumn("Label").AsString().Nullable() - .WithColumn("Disambiguation").AsString().Nullable() - .WithColumn("Country").AsString().Nullable() - .WithColumn("ReleaseDate").AsDateTime().Nullable() - .WithColumn("Media").AsString().Nullable() - .WithColumn("TrackCount").AsInt32().Nullable() - .WithColumn("Monitored").AsBoolean() - .WithColumn("OldForeignReleaseIds").AsString().WithDefaultValue("[]"); - - Create.TableForModel("Tracks") - .WithColumn("ForeignTrackId").AsString().Unique() - .WithColumn("Title").AsString().Nullable() - .WithColumn("Explicit").AsBoolean() - .WithColumn("TrackFileId").AsInt32().Nullable().Indexed() - .WithColumn("Ratings").AsString().Nullable() - .WithColumn("Duration").AsInt32().WithDefaultValue(0) - .WithColumn("MediumNumber").AsInt32().WithDefaultValue(0) - .WithColumn("AbsoluteTrackNumber").AsInt32().WithDefaultValue(0) - .WithColumn("TrackNumber").AsString().Nullable() - .WithColumn("ForeignRecordingId").AsString().WithDefaultValue("0") - .WithColumn("AlbumReleaseId").AsInt32().WithDefaultValue(0) - .WithColumn("ArtistMetadataId").AsInt32().WithDefaultValue(0) - .WithColumn("OldForeignRecordingIds").AsString().WithDefaultValue("[]") - .WithColumn("OldForeignTrackIds").AsString().WithDefaultValue("[]"); - - Create.Index().OnTable("Tracks").OnColumn("ArtistId").Ascending() - .OnColumn("AlbumId").Ascending() - .OnColumn("TrackNumber").Ascending(); - - Create.TableForModel("TrackFiles") - .WithColumn("AlbumId").AsInt32().Indexed() + Create.TableForModel("BookFiles") + .WithColumn("BookId").AsInt32().Indexed() + .WithColumn("CalibreId").AsInt32() .WithColumn("Quality").AsString() .WithColumn("Size").AsInt64() .WithColumn("SceneName").AsString().Nullable() @@ -125,9 +110,8 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Data").AsString() .WithColumn("EventType").AsInt32().Nullable().Indexed() .WithColumn("DownloadId").AsString().Nullable().Indexed() - .WithColumn("ArtistId").AsInt32().WithDefaultValue(0) - .WithColumn("AlbumId").AsInt32().Indexed().WithDefaultValue(0) - .WithColumn("TrackId").AsInt32().WithDefaultValue(0); + .WithColumn("AuthorId").AsInt32().WithDefaultValue(0) + .WithColumn("BookId").AsInt32().Indexed().WithDefaultValue(0); Create.TableForModel("Notifications") .WithColumn("Name").AsString() @@ -168,9 +152,13 @@ namespace NzbDrone.Core.Datastore.Migration Create.TableForModel("MetadataProfiles") .WithColumn("Name").AsString().Unique() - .WithColumn("PrimaryAlbumTypes").AsString() - .WithColumn("SecondaryAlbumTypes").AsString() - .WithColumn("ReleaseStatuses").AsString().WithDefaultValue(""); + .WithColumn("MinRating").AsDouble() + .WithColumn("MinRatingCount").AsInt32() + .WithColumn("SkipMissingDate").AsBoolean() + .WithColumn("SkipMissingIsbn").AsBoolean() + .WithColumn("SkipPartsAndSets").AsBoolean() + .WithColumn("SkipSeriesSecondary").AsBoolean() + .WithColumn("AllowedLanguages").AsString().Nullable(); Create.TableForModel("QualityDefinitions") .WithColumn("Quality").AsInt32().Unique() @@ -196,8 +184,8 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Indexer").AsString().Nullable() .WithColumn("Message").AsString().Nullable() .WithColumn("TorrentInfoHash").AsString().Nullable() - .WithColumn("ArtistId").AsInt32().WithDefaultValue(0) - .WithColumn("AlbumIds").AsString().WithDefaultValue(""); + .WithColumn("AuthorId").AsInt32().WithDefaultValue(0) + .WithColumn("BookIds").AsString().WithDefaultValue(""); Create.TableForModel("Metadata") .WithColumn("Enable").AsBoolean().NotNullable() @@ -207,12 +195,12 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("ConfigContract").AsString().NotNullable(); Create.TableForModel("MetadataFiles") - .WithColumn("ArtistId").AsInt32().NotNullable() + .WithColumn("AuthorId").AsInt32().NotNullable() .WithColumn("Consumer").AsString().NotNullable() .WithColumn("Type").AsInt32().NotNullable() .WithColumn("RelativePath").AsString().NotNullable() .WithColumn("LastUpdated").AsDateTime().NotNullable() - .WithColumn("AlbumId").AsInt32().Nullable() + .WithColumn("BookId").AsInt32().Nullable() .WithColumn("TrackFileId").AsInt32().Nullable() .WithColumn("Hash").AsString().Nullable() .WithColumn("Added").AsDateTime().Nullable() @@ -230,7 +218,7 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Title").AsString() .WithColumn("Added").AsDateTime() .WithColumn("Release").AsString() - .WithColumn("ArtistId").AsInt32().WithDefaultValue(0) + .WithColumn("AuthorId").AsInt32().WithDefaultValue(0) .WithColumn("ParsedAlbumInfo").AsString().WithDefaultValue("") .WithColumn("Reason").AsInt32().WithDefaultValue(0); @@ -284,17 +272,8 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("LastRssSyncReleaseInfo").AsString().Nullable(); Create.TableForModel("ExtraFiles") - .WithColumn("ArtistId").AsInt32().NotNullable() - .WithColumn("AlbumId").AsInt32().NotNullable() - .WithColumn("TrackFileId").AsInt32().NotNullable() - .WithColumn("RelativePath").AsString().NotNullable() - .WithColumn("Extension").AsString().NotNullable() - .WithColumn("Added").AsDateTime().NotNullable() - .WithColumn("LastUpdated").AsDateTime().NotNullable(); - - Create.TableForModel("LyricFiles") - .WithColumn("ArtistId").AsInt32().NotNullable() - .WithColumn("AlbumId").AsInt32().NotNullable() + .WithColumn("AuthorId").AsInt32().NotNullable() + .WithColumn("BookId").AsInt32().NotNullable() .WithColumn("TrackFileId").AsInt32().NotNullable() .WithColumn("RelativePath").AsString().NotNullable() .WithColumn("Extension").AsString().NotNullable() @@ -337,23 +316,20 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Label").AsString().NotNullable() .WithColumn("Filters").AsString().NotNullable(); - Create.Index().OnTable("Albums").OnColumn("ArtistId"); - Create.Index().OnTable("Albums").OnColumn("ArtistId").Ascending() - .OnColumn("ReleaseDate").Ascending(); + Create.Index().OnTable("Books").OnColumn("AuthorId"); + Create.Index().OnTable("Books").OnColumn("AuthorId").Ascending() + .OnColumn("ReleaseDate").Ascending(); - Delete.Index().OnTable("History").OnColumn("AlbumId"); - Create.Index().OnTable("History").OnColumn("AlbumId").Ascending() + Delete.Index().OnTable("History").OnColumn("BookId"); + Create.Index().OnTable("History").OnColumn("BookId").Ascending() .OnColumn("Date").Descending(); Delete.Index().OnTable("History").OnColumn("DownloadId"); Create.Index().OnTable("History").OnColumn("DownloadId").Ascending() .OnColumn("Date").Descending(); - Create.Index().OnTable("Artists").OnColumn("Monitored").Ascending(); - Create.Index().OnTable("Albums").OnColumn("ArtistMetadataId").Ascending(); - Create.Index().OnTable("Tracks").OnColumn("ArtistMetadataId").Ascending(); - Create.Index().OnTable("Tracks").OnColumn("AlbumReleaseId").Ascending(); - Create.Index().OnTable("Tracks").OnColumn("ForeignRecordingId").Ascending(); + Create.Index().OnTable("Authors").OnColumn("Monitored").Ascending(); + Create.Index().OnTable("Books").OnColumn("AuthorMetadataId").Ascending(); Insert.IntoTable("DelayProfiles").Row(new { diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index f8e773da0..945ad3c1f 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -10,7 +10,6 @@ using NzbDrone.Core.CustomFilters; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; -using NzbDrone.Core.Extras.Lyrics; using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Others; @@ -95,64 +94,53 @@ namespace NzbDrone.Core.Datastore Mapper.Entity("History").RegisterModel(); - Mapper.Entity("Artists") + Mapper.Entity("Authors") .Ignore(s => s.RootFolderPath) .Ignore(s => s.Name) - .Ignore(s => s.ForeignArtistId) - .HasOne(a => a.Metadata, a => a.ArtistMetadataId) + .Ignore(s => s.ForeignAuthorId) + .HasOne(a => a.Metadata, a => a.AuthorMetadataId) .HasOne(a => a.QualityProfile, a => a.QualityProfileId) .HasOne(s => s.MetadataProfile, s => s.MetadataProfileId) - .LazyLoad(a => a.Albums, (db, a) => db.Query(new SqlBuilder().Where(rg => rg.ArtistMetadataId == a.Id)).ToList(), a => a.Id > 0); - - Mapper.Entity("ArtistMetadata").RegisterModel(); - - Mapper.Entity("Albums").RegisterModel() - .Ignore(x => x.ArtistId) - .HasOne(r => r.ArtistMetadata, r => r.ArtistMetadataId) - .LazyLoad(a => a.AlbumReleases, (db, album) => db.Query(new SqlBuilder().Where(r => r.AlbumId == album.Id)).ToList(), a => a.Id > 0) - .LazyLoad(a => a.Artist, + .LazyLoad(a => a.Books, (db, a) => db.Query(new SqlBuilder().Where(rg => rg.AuthorMetadataId == a.Id)).ToList(), a => a.Id > 0); + + Mapper.Entity("Series").RegisterModel() + .LazyLoad(s => s.LinkItems, + (db, series) => db.Query(new SqlBuilder().Where(s => s.SeriesId == series.Id)).ToList(), + s => s.Id > 0) + .LazyLoad(s => s.Books, + (db, series) => db.Query(new SqlBuilder() + .Join((l, r) => l.Id == r.BookId) + .Join((l, r) => l.SeriesId == r.Id) + .Where(s => s.Id == series.Id)).ToList(), + s => s.Id > 0); + + Mapper.Entity("SeriesBookLink").RegisterModel(); + + Mapper.Entity("AuthorMetadata").RegisterModel(); + + Mapper.Entity("Books").RegisterModel() + .Ignore(x => x.AuthorId) + .HasOne(r => r.AuthorMetadata, r => r.AuthorMetadataId) + .LazyLoad(x => x.BookFiles, + (db, book) => db.Query(new SqlBuilder() + .Join((l, r) => l.BookId == r.Id) + .Where(b => b.Id == book.Id)).ToList(), + b => b.Id > 0) + .LazyLoad(a => a.Author, (db, album) => ArtistRepository.Query(db, new SqlBuilder() - .Join((a, m) => a.ArtistMetadataId == m.Id) - .Where(a => a.ArtistMetadataId == album.ArtistMetadataId)).SingleOrDefault(), - a => a.ArtistMetadataId > 0); - - Mapper.Entity("AlbumReleases").RegisterModel() - .HasOne(r => r.Album, r => r.AlbumId) - .LazyLoad(x => x.Tracks, (db, release) => db.Query(new SqlBuilder().Where(t => t.AlbumReleaseId == release.Id)).ToList(), r => r.Id > 0); - - Mapper.Entity("Tracks").RegisterModel() - .Ignore(t => t.HasFile) - .Ignore(t => t.AlbumId) - .HasOne(track => track.AlbumRelease, track => track.AlbumReleaseId) - .HasOne(track => track.ArtistMetadata, track => track.ArtistMetadataId) - .LazyLoad(t => t.TrackFile, - (db, track) => MediaFileRepository.Query(db, - new SqlBuilder() - .Join((l, r) => l.Id == r.TrackFileId) - .Join((l, r) => l.AlbumId == r.Id) - .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .Join((l, r) => l.ArtistMetadataId == r.Id) - .Where(t => t.Id == track.TrackFileId)).SingleOrDefault(), - t => t.TrackFileId > 0) - .LazyLoad(x => x.Artist, - (db, t) => ArtistRepository.Query(db, - new SqlBuilder() - .Join((a, m) => a.ArtistMetadataId == m.Id) - .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .Join((l, r) => l.Id == r.AlbumId) - .Where(r => r.Id == t.AlbumReleaseId)).SingleOrDefault(), - t => t.Id > 0); + .Join((a, m) => a.AuthorMetadataId == m.Id) + .Where(a => a.AuthorMetadataId == album.AuthorMetadataId)).SingleOrDefault(), + a => a.AuthorMetadataId > 0); - Mapper.Entity("TrackFiles").RegisterModel() - .HasOne(f => f.Album, f => f.AlbumId) - .LazyLoad(x => x.Tracks, (db, file) => db.Query(new SqlBuilder().Where(t => t.TrackFileId == file.Id)).ToList(), x => x.Id > 0) + Mapper.Entity("BookFiles").RegisterModel() + .HasOne(f => f.Album, f => f.BookId) .LazyLoad(x => x.Artist, (db, f) => ArtistRepository.Query(db, new SqlBuilder() - .Join((a, m) => a.ArtistMetadataId == m.Id) - .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .Where(a => a.Id == f.AlbumId)).SingleOrDefault(), + .Join((a, m) => a.AuthorMetadataId == m.Id) + .Join((l, r) => l.AuthorMetadataId == r.AuthorMetadataId) + .Where(a => a.Id == f.BookId)).SingleOrDefault(), t => t.Id > 0); Mapper.Entity("QualityDefinitions").RegisterModel() @@ -167,7 +155,6 @@ namespace NzbDrone.Core.Datastore Mapper.Entity("Blacklist").RegisterModel(); Mapper.Entity("MetadataFiles").RegisterModel(); - Mapper.Entity("LyricFiles").RegisterModel(); Mapper.Entity("ExtraFiles").RegisterModel(); Mapper.Entity("PendingReleases").RegisterModel() @@ -206,9 +193,6 @@ namespace NzbDrone.Core.Datastore SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); - SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>(new PrimaryAlbumTypeIntConverter())); - SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>(new SecondaryAlbumTypeIntConverter())); - SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>(new ReleaseStatusIntConverter())); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); diff --git a/src/NzbDrone.Core/Datastore/WhereBuilder.cs b/src/NzbDrone.Core/Datastore/WhereBuilder.cs index 484d5e1bf..3c018d0ad 100644 --- a/src/NzbDrone.Core/Datastore/WhereBuilder.cs +++ b/src/NzbDrone.Core/Datastore/WhereBuilder.cs @@ -316,7 +316,20 @@ namespace NzbDrone.Core.Datastore _sb.Append(" IN "); - Visit(list); + // hardcode the integer list if it exists to bypass parameter limit + if (item.Type == typeof(int) && TryGetRightValue(list, out var value)) + { + var items = (IEnumerable)value; + _sb.Append("("); + _sb.Append(string.Join(", ", items)); + _sb.Append(")"); + + _gotConcreteValue = true; + } + else + { + Visit(list); + } _sb.Append(")"); } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 897db56e4..2917ed153 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -70,29 +70,23 @@ namespace NzbDrone.Core.DecisionEngine { var parsedAlbumInfo = Parser.Parser.ParseAlbumTitle(report.Title); - if (parsedAlbumInfo == null && searchCriteria != null) + if (parsedAlbumInfo == null) { - parsedAlbumInfo = Parser.Parser.ParseAlbumTitleWithSearchCriteria(report.Title, - searchCriteria.Artist, - searchCriteria.Albums); + if (searchCriteria != null) + { + parsedAlbumInfo = Parser.Parser.ParseAlbumTitleWithSearchCriteria(report.Title, + searchCriteria.Artist, + searchCriteria.Albums); + } + else + { + // try parsing fuzzy + parsedAlbumInfo = _parsingService.ParseAlbumTitleFuzzy(report.Title); + } } if (parsedAlbumInfo != null) { - // TODO: Artist Data Augment without calling to parse title again - //if (!report.Artist.IsNullOrWhiteSpace()) - //{ - // if (parsedAlbumInfo.ArtistName.IsNullOrWhiteSpace() || _parsingService.GetArtist(parsedAlbumInfo.ArtistName) == null) - // { - // parsedAlbumInfo.ArtistName = report.Artist; - // } - //} - - // TODO: Replace Parsed AlbumTitle with metadata Title if Parsed AlbumTitle not a valid match - //if (!report.Album.IsNullOrWhiteSpace()) - //{ - // parsedAlbumInfo.AlbumTitle = report.Album; - //} if (!parsedAlbumInfo.ArtistName.IsNullOrWhiteSpace()) { var remoteAlbum = _parsingService.Map(parsedAlbumInfo, searchCriteria); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index f7b55535b..616c5ba45 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -24,7 +24,7 @@ namespace NzbDrone.Core.DecisionEngine public List PrioritizeDecisions(List decisions) { return decisions.Where(c => c.RemoteAlbum.DownloadAllowed) - .GroupBy(c => c.RemoteAlbum.Artist.Id, (artistId, downloadDecisions) => + .GroupBy(c => c.RemoteAlbum.Artist.Id, (authorId, downloadDecisions) => { return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_configService, _delayProfileService)); }) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 98e76ae7a..cf9266d1e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -23,6 +23,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) { + _logger.Debug("size restriction not implemented"); + return Decision.Accept(); + + /* _logger.Debug("Beginning size check for: {0}", subject); var quality = subject.ParsedAlbumInfo.Quality.Quality; @@ -38,17 +42,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (qualityDefinition.MinSize.HasValue) { var minSize = qualityDefinition.MinSize.Value.Kilobits(); - var minReleaseDuration = subject.Albums.Select(a => a.AlbumReleases.Value.Where(r => r.Monitored || a.AnyReleaseOk).Select(r => r.Duration).Min()).Sum() / 1000; - - //Multiply minSize by smallest release duration - minSize = minSize * minReleaseDuration; //If the parsed size is smaller than minSize we don't want it if (subject.Release.Size < minSize) { - var runtimeMessage = $"{minReleaseDuration}sec"; - - _logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, minSize, runtimeMessage); + _logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes), rejecting.", subject, subject.Release.Size, minSize); return Decision.Reject("{0} is smaller than minimum allowed {1}", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix()); } } @@ -60,23 +58,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications else { var maxSize = qualityDefinition.MaxSize.Value.Kilobits(); - var maxReleaseDuration = subject.Albums.Select(a => a.AlbumReleases.Value.Where(r => r.Monitored || a.AnyReleaseOk).Select(r => r.Duration).Max()).Sum() / 1000; - - //Multiply maxSize by Album.Duration - maxSize = maxSize * maxReleaseDuration; //If the parsed size is greater than maxSize we don't want it if (subject.Release.Size > maxSize) { - var runtimeMessage = $"{maxReleaseDuration}sec"; - - _logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, maxSize, runtimeMessage); + _logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} bytes), rejecting.", subject, subject.Release.Size, maxSize); return Decision.Reject("{0} is larger than maximum allowed {1}", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix()); } } _logger.Debug("Item: {0}, meets size constraints.", subject); return Decision.Accept(); + */ } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index 9dbd13506..46f4d88ca 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -1,36 +1,25 @@ -using System; +using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Releases; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.DecisionEngine.Specifications { public class CutoffSpecification : IDecisionEngineSpecification { private readonly UpgradableSpecification _upgradableSpecification; - private readonly IMediaFileService _mediaFileService; - private readonly ITrackService _trackService; private readonly Logger _logger; - private readonly ICached _missingFilesCache; private readonly IPreferredWordService _preferredWordServiceCalculator; public CutoffSpecification(UpgradableSpecification upgradableSpecification, - Logger logger, - ICacheManager cacheManager, - IMediaFileService mediaFileService, IPreferredWordService preferredWordServiceCalculator, - ITrackService trackService) + Logger logger) { _upgradableSpecification = upgradableSpecification; - _mediaFileService = mediaFileService; - _trackService = trackService; - _missingFilesCache = cacheManager.GetCache(GetType()); _preferredWordServiceCalculator = preferredWordServiceCalculator; _logger = logger; } @@ -42,33 +31,25 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var qualityProfile = subject.Artist.QualityProfile.Value; - foreach (var album in subject.Albums) + foreach (var file in subject.Albums.SelectMany(b => b.BookFiles.Value)) { - var tracksMissing = _missingFilesCache.Get(album.Id.ToString(), - () => _trackService.TracksWithoutFiles(album.Id).Any(), - TimeSpan.FromSeconds(30)); - var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id); + // Get a distinct list of all current track qualities for a given album + var currentQualities = new List { file.Quality }; - if (!tracksMissing && trackFiles.Any()) - { - // Get a distinct list of all current track qualities for a given album - var currentQualities = trackFiles.Select(c => c.Quality).Distinct().ToList(); - - _logger.Debug("Comparing file quality with report. Existing files contain {0}", currentQualities.ConcatToString()); + _logger.Debug("Comparing file quality with report. Existing files contain {0}", currentQualities.ConcatToString()); - if (!_upgradableSpecification.CutoffNotMet(qualityProfile, - currentQualities, - _preferredWordServiceCalculator.Calculate(subject.Artist, trackFiles[0].GetSceneOrFileName()), - subject.ParsedAlbumInfo.Quality, - subject.PreferredWordScore)) - { - _logger.Debug("Cutoff already met by existing files, rejecting."); + if (!_upgradableSpecification.CutoffNotMet(qualityProfile, + currentQualities, + _preferredWordServiceCalculator.Calculate(subject.Artist, file.GetSceneOrFileName()), + subject.ParsedAlbumInfo.Quality, + subject.PreferredWordScore)) + { + _logger.Debug("Cutoff already met by existing files, rejecting."); - var qualityCutoffIndex = qualityProfile.GetIndex(qualityProfile.Cutoff); - var qualityCutoff = qualityProfile.Items[qualityCutoffIndex.Index]; + var qualityCutoffIndex = qualityProfile.GetIndex(qualityProfile.Cutoff); + var qualityCutoff = qualityProfile.Items[qualityCutoffIndex.Index]; - return Decision.Reject("Existing files meets cutoff: {0}", qualityCutoff); - } + return Decision.Reject("Existing files meets cutoff: {0}", qualityCutoff); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index 623d66b97..8e12e3d24 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -92,9 +92,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync return Decision.Accept(); } - var albumIds = subject.Albums.Select(e => e.Id); + var bookIds = subject.Albums.Select(e => e.Id); - var oldest = _pendingReleaseService.OldestPendingRelease(subject.Artist.Id, albumIds.ToArray()); + var oldest = _pendingReleaseService.OldestPendingRelease(subject.Artist.Id, bookIds.ToArray()); if (oldest != null && oldest.Release.AgeMinutes > delay) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs index 6543cf139..6faf03b44 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs @@ -64,7 +64,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync return Decision.Accept(); } - private bool IsTrackFileMissing(Artist artist, TrackFile trackFile) + private bool IsTrackFileMissing(Author artist, BookFile trackFile) { return !_diskProvider.FileExists(trackFile.Path); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksGrabSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksGrabSpecification.cs deleted file mode 100644 index 8f03c4cee..000000000 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksGrabSpecification.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using NLog; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.DecisionEngine.Specifications -{ - public class SameTracksGrabSpecification : IDecisionEngineSpecification - { - private readonly SameTracksSpecification _sameTracksSpecification; - private readonly Logger _logger; - - public SameTracksGrabSpecification(SameTracksSpecification sameTracksSpecification, Logger logger) - { - _sameTracksSpecification = sameTracksSpecification; - _logger = logger; - } - - public SpecificationPriority Priority => SpecificationPriority.Default; - public RejectionType Type => RejectionType.Permanent; - - public Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) - { - throw new NotImplementedException(); - - // TODO: Rework for Tracks if we can parse from release details. - } - } -} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksSpecification.cs deleted file mode 100644 index 7cdbd5146..000000000 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SameTracksSpecification.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.DecisionEngine.Specifications -{ - public class SameTracksSpecification - { - private readonly ITrackService _trackService; - - public SameTracksSpecification(ITrackService trackService) - { - _trackService = trackService; - } - - public bool IsSatisfiedBy(List tracks) - { - var trackIds = tracks.SelectList(e => e.Id); - var trackFileIds = tracks.Where(c => c.TrackFileId != 0).Select(c => c.TrackFileId).Distinct(); - - foreach (var trackFileId in trackFileIds) - { - var tracksInFile = _trackService.GetTracksByFileId(trackFileId); - - if (tracksInFile.Select(e => e.Id).Except(trackIds).Any()) - { - return false; - } - } - - return true; - } - } -} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeAllowedSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeAllowedSpecification.cs index 638ebcd30..944cf6f84 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeAllowedSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeAllowedSpecification.cs @@ -1,33 +1,22 @@ -using System; +using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.DecisionEngine.Specifications { public class UpgradeAllowedSpecification : IDecisionEngineSpecification { private readonly UpgradableSpecification _upgradableSpecification; - private readonly IMediaFileService _mediaFileService; - private readonly ITrackService _trackService; private readonly Logger _logger; - private readonly ICached _missingFilesCache; public UpgradeAllowedSpecification(UpgradableSpecification upgradableSpecification, - Logger logger, - ICacheManager cacheManager, - IMediaFileService mediaFileService, - ITrackService trackService) + Logger logger) { _upgradableSpecification = upgradableSpecification; - _mediaFileService = mediaFileService; - _trackService = trackService; - _missingFilesCache = cacheManager.GetCache(GetType()); _logger = logger; } @@ -38,29 +27,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var qualityProfile = subject.Artist.QualityProfile.Value; - foreach (var album in subject.Albums) + foreach (var file in subject.Albums.SelectMany(b => b.BookFiles.Value)) { - var tracksMissing = _missingFilesCache.Get(album.Id.ToString(), - () => _trackService.TracksWithoutFiles(album.Id).Any(), - TimeSpan.FromSeconds(30)); - - var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id); - - if (!tracksMissing && trackFiles.Any()) + if (file == null) { - // Get a distinct list of all current track qualities for a given album - var currentQualities = trackFiles.Select(c => c.Quality).Distinct().ToList(); + _logger.Debug("File is no longer available, skipping this file."); + continue; + } - _logger.Debug("Comparing file quality with report. Existing files contain {0}", currentQualities.ConcatToString()); + // Get a distinct list of all current track qualities for a given album + var currentQualities = new List { file.Quality }; - if (!_upgradableSpecification.IsUpgradeAllowed(qualityProfile, + _logger.Debug("Comparing file quality with report. Existing files contain {0}", currentQualities.ConcatToString()); + + if (!_upgradableSpecification.IsUpgradeAllowed(qualityProfile, currentQualities, subject.ParsedAlbumInfo.Quality)) - { - _logger.Debug("Upgrading is not allowed by the quality profile"); + { + _logger.Debug("Upgrading is not allowed by the quality profile"); - return Decision.Reject("Existing files and the Quality profile does not allow upgrades"); - } + return Decision.Reject("Existing files and the Quality profile does not allow upgrades"); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 6f0d485b4..ba15adff5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -1,38 +1,26 @@ -using System; +using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Releases; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.DecisionEngine.Specifications { public class UpgradeDiskSpecification : IDecisionEngineSpecification { - private readonly IMediaFileService _mediaFileService; - private readonly ITrackService _trackService; private readonly UpgradableSpecification _upgradableSpecification; private readonly IPreferredWordService _preferredWordServiceCalculator; private readonly Logger _logger; - private readonly ICached _missingFilesCache; public UpgradeDiskSpecification(UpgradableSpecification qualityUpgradableSpecification, - IMediaFileService mediaFileService, - ITrackService trackService, - ICacheManager cacheManager, IPreferredWordService preferredWordServiceCalculator, Logger logger) { _upgradableSpecification = qualityUpgradableSpecification; - _mediaFileService = mediaFileService; - _trackService = trackService; _preferredWordServiceCalculator = preferredWordServiceCalculator; _logger = logger; - _missingFilesCache = cacheManager.GetCache(GetType()); } public SpecificationPriority Priority => SpecificationPriority.Default; @@ -40,25 +28,23 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) { - foreach (var album in subject.Albums) + foreach (var file in subject.Albums.SelectMany(c => c.BookFiles.Value)) { - var tracksMissing = _missingFilesCache.Get(album.Id.ToString(), - () => _trackService.TracksWithoutFiles(album.Id).Any(), - TimeSpan.FromSeconds(30)); - var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id); - - if (!tracksMissing && trackFiles.Any()) + if (file == null) { - var currentQualities = trackFiles.Select(c => c.Quality).Distinct().ToList(); + _logger.Debug("File is no longer available, skipping this file."); + continue; + } - if (!_upgradableSpecification.IsUpgradable(subject.Artist.QualityProfile, - currentQualities, - _preferredWordServiceCalculator.Calculate(subject.Artist, trackFiles[0].GetSceneOrFileName()), - subject.ParsedAlbumInfo.Quality, - subject.PreferredWordScore)) - { - return Decision.Reject("Existing files on disk is of equal or higher preference: {0}", currentQualities.ConcatToString()); - } + _logger.Debug("Comparing file quality and language with report. Existing file is {0}", file.Quality); + + if (!_upgradableSpecification.IsUpgradable(subject.Artist.QualityProfile, + new List { file.Quality }, + _preferredWordServiceCalculator.Calculate(subject.Artist, file.GetSceneOrFileName()), + subject.ParsedAlbumInfo.Quality, + subject.PreferredWordScore)) + { + return Decision.Reject("Existing file on disk is of equal or higher preference: {0}", file.Quality); } } diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs index 829de05d2..145b869ec 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex { Host = "localhost"; Port = 4321; - MusicCategory = "Music"; + MusicCategory = "Readarr"; RecentTvPriority = (int)NzbVortexPriority.Normal; OlderTvPriority = (int)NzbVortexPriority.Normal; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index 4145d8436..44cffddce 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { Host = "localhost"; Port = 6789; - MusicCategory = "Music"; + MusicCategory = "Readarr"; Username = "nzbget"; Password = "tegbzn6789"; RecentTvPriority = (int)NzbgetPriority.Normal; diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index ea14a5a2f..be7414463 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { Host = "localhost"; Port = 8080; - MusicCategory = "music"; + MusicCategory = "Readarr"; RecentTvPriority = (int)SabnzbdPriority.Default; OlderTvPriority = (int)SabnzbdPriority.Default; } diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 0806797a1..c244ec066 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using NzbDrone.Common.EnvironmentInfo; @@ -83,7 +84,7 @@ namespace NzbDrone.Core.Download { if (historyItem != null) { - artist = _artistService.GetArtist(historyItem.ArtistId); + artist = _artistService.GetArtist(historyItem.AuthorId); } if (artist == null) @@ -111,59 +112,65 @@ namespace NzbDrone.Core.Download return; } - var allTracksImported = importResults.All(c => c.Result == ImportResultType.Imported) || - importResults.Count(c => c.Result == ImportResultType.Imported) >= - Math.Max(1, trackedDownload.RemoteAlbum.Albums.Sum(x => x.AlbumReleases.Value.Where(y => y.Monitored).Sum(z => z.TrackCount))); + if (VerifyImport(trackedDownload, importResults)) + { + return; + } - Console.WriteLine($"allimported: {allTracksImported}"); - Console.WriteLine($"count: {importResults.Count(c => c.Result == ImportResultType.Imported)}"); - Console.WriteLine($"max: {Math.Max(1, trackedDownload.RemoteAlbum.Albums.Sum(x => x.AlbumReleases.Value.Where(y => y.Monitored).Sum(z => z.TrackCount)))}"); + trackedDownload.State = TrackedDownloadState.ImportPending; - if (allTracksImported) + if (importResults.Any(c => c.Result != ImportResultType.Imported)) + { + trackedDownload.State = TrackedDownloadState.ImportFailed; + var statusMessages = importResults + .Where(v => v.Result != ImportResultType.Imported) + .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.Item.Path), v.Errors)) + .ToArray(); + + trackedDownload.Warn(statusMessages); + _eventAggregator.PublishEvent(new AlbumImportIncompleteEvent(trackedDownload)); + return; + } + } + + public bool VerifyImport(TrackedDownload trackedDownload, List importResults) + { + var allItemsImported = importResults.Where(c => c.Result == ImportResultType.Imported) + .Select(c => c.ImportDecision.Item.Album) + .Count() >= Math.Max(1, trackedDownload.RemoteAlbum.Albums.Count); + + if (allItemsImported) { trackedDownload.State = TrackedDownloadState.Imported; _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); - return; + return true; } - // Double check if all albums were imported by checking the history if at least one + // Double check if all episodes were imported by checking the history if at least one // file was imported. This will allow the decision engine to reject already imported - // albums and still mark the download complete when all files are imported. + // episode files and still mark the download complete when all files are imported. + + // EDGE CASE: This process relies on EpisodeIds being consistent between executions, if a series is updated + // and an episode is removed, but later comes back with a different ID then Sonarr will treat it as incomplete. + // Since imports should be relatively fast and these types of data changes are infrequent this should be quite + // safe, but commenting for future benefit. if (importResults.Any(c => c.Result == ImportResultType.Imported)) { var historyItems = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId) - .OrderByDescending(h => h.Date) - .ToList(); + .OrderByDescending(h => h.Date) + .ToList(); - var allTracksImportedInHistory = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems); + var allEpisodesImportedInHistory = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems); - if (allTracksImportedInHistory) + if (allEpisodesImportedInHistory) { trackedDownload.State = TrackedDownloadState.Imported; _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); - return; + return true; } } - trackedDownload.State = TrackedDownloadState.ImportPending; - - if (importResults.Empty()) - { - trackedDownload.Warn("No files found are eligible for import in {0}", outputPath); - } - - if (importResults.Any(c => c.Result != ImportResultType.Imported)) - { - trackedDownload.State = TrackedDownloadState.ImportFailed; - var statusMessages = importResults - .Where(v => v.Result != ImportResultType.Imported) - .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.Item.Path), v.Errors)) - .ToArray(); - - trackedDownload.Warn(statusMessages); - _eventAggregator.PublishEvent(new AlbumImportIncompleteEvent(trackedDownload)); - return; - } + return false; } } } diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index 40fadd628..870045ba2 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -12,8 +12,8 @@ namespace NzbDrone.Core.Download Data = new Dictionary(); } - public int ArtistId { get; set; } - public List AlbumIds { get; set; } + public int AuthorId { get; set; } + public List BookIds { get; set; } public QualityModel Quality { get; set; } public string SourceTitle { get; set; } public string DownloadClient { get; set; } diff --git a/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs b/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs index 7373266a7..c8082941a 100644 --- a/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs @@ -6,8 +6,8 @@ namespace NzbDrone.Core.Download { public class DownloadIgnoredEvent : IEvent { - public int ArtistId { get; set; } - public List AlbumIds { get; set; } + public int AuthorId { get; set; } + public List BookIds { get; set; } public QualityModel Quality { get; set; } public string SourceTitle { get; set; } public string DownloadClient { get; set; } diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index fe5063e64..9dde73210 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -115,8 +115,8 @@ namespace NzbDrone.Core.Download var downloadFailedEvent = new DownloadFailedEvent { - ArtistId = historyItem.ArtistId, - AlbumIds = historyItems.Select(h => h.AlbumId).ToList(), + AuthorId = historyItem.AuthorId, + BookIds = historyItems.Select(h => h.BookId).ToList(), Quality = historyItem.Quality, SourceTitle = historyItem.SourceTitle, DownloadClient = historyItem.Data.GetValueOrDefault(History.History.DOWNLOAD_CLIENT), diff --git a/src/NzbDrone.Core/Download/IgnoredDownloadService.cs b/src/NzbDrone.Core/Download/IgnoredDownloadService.cs index ecc051162..cd2828bf3 100644 --- a/src/NzbDrone.Core/Download/IgnoredDownloadService.cs +++ b/src/NzbDrone.Core/Download/IgnoredDownloadService.cs @@ -36,8 +36,8 @@ namespace NzbDrone.Core.Download var downloadIgnoredEvent = new DownloadIgnoredEvent { - ArtistId = artist.Id, - AlbumIds = albums.Select(e => e.Id).ToList(), + AuthorId = artist.Id, + BookIds = albums.Select(e => e.Id).ToList(), Quality = trackedDownload.RemoteAlbum.ParsedAlbumInfo.Quality, SourceTitle = trackedDownload.DownloadItem.Title, DownloadClient = trackedDownload.DownloadItem.DownloadClient, diff --git a/src/NzbDrone.Core/Download/Pending/PendingRelease.cs b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs index a9273ec2e..d1062f911 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingRelease.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.Download.Pending { public class PendingRelease : ModelBase { - public int ArtistId { get; set; } + public int AuthorId { get; set; } public string Title { get; set; } public DateTime Added { get; set; } public ParsedAlbumInfo ParsedAlbumInfo { get; set; } diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs index 8052ea01b..d8dc2af4b 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs @@ -6,8 +6,8 @@ namespace NzbDrone.Core.Download.Pending { public interface IPendingReleaseRepository : IBasicRepository { - void DeleteByArtistId(int artistId); - List AllByArtistId(int artistId); + void DeleteByAuthorId(int authorId); + List AllByAuthorId(int authorId); List WithoutFallback(); } @@ -18,14 +18,14 @@ namespace NzbDrone.Core.Download.Pending { } - public void DeleteByArtistId(int artistId) + public void DeleteByAuthorId(int authorId) { - Delete(artistId); + Delete(authorId); } - public List AllByArtistId(int artistId) + public List AllByAuthorId(int authorId) { - return Query(p => p.ArtistId == artistId); + return Query(p => p.AuthorId == authorId); } public List WithoutFallback() diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index 19a19ce3a..eb7f2370d 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -23,11 +23,11 @@ namespace NzbDrone.Core.Download.Pending void Add(DownloadDecision decision, PendingReleaseReason reason); void AddMany(List> decisions); List GetPending(); - List GetPendingRemoteAlbums(int artistId); + List GetPendingRemoteAlbums(int authorId); List GetPendingQueue(); Queue.Queue FindPendingQueueItem(int queueId); void RemovePendingQueueItems(int queueId); - RemoteAlbum OldestPendingRelease(int artistId, int[] albumIds); + RemoteAlbum OldestPendingRelease(int authorId, int[] bookIds); } public class PendingReleaseService : IPendingReleaseService, @@ -76,7 +76,7 @@ namespace NzbDrone.Core.Download.Pending foreach (var artistDecisions in decisions.GroupBy(v => v.Item1.RemoteAlbum.Artist.Id)) { var artist = artistDecisions.First().Item1.RemoteAlbum.Artist; - var alreadyPending = _repository.AllByArtistId(artist.Id); + var alreadyPending = _repository.AllByAuthorId(artist.Id); alreadyPending = IncludeRemoteAlbums(alreadyPending, artistDecisions.ToDictionaryIgnoreDuplicates(v => v.Item1.RemoteAlbum.Release.Title, v => v.Item1.RemoteAlbum)); var alreadyPendingByAlbum = CreateAlbumLookup(alreadyPending); @@ -86,9 +86,9 @@ namespace NzbDrone.Core.Download.Pending var decision = pair.Item1; var reason = pair.Item2; - var albumIds = decision.RemoteAlbum.Albums.Select(e => e.Id); + var bookIds = decision.RemoteAlbum.Albums.Select(e => e.Id); - var existingReports = albumIds.SelectMany(v => alreadyPendingByAlbum[v] ?? Enumerable.Empty()) + var existingReports = bookIds.SelectMany(v => alreadyPendingByAlbum[v] ?? Enumerable.Empty()) .Distinct().ToList(); var matchingReports = existingReports.Where(MatchingReleasePredicate(decision.RemoteAlbum.Release)).ToList(); @@ -155,9 +155,9 @@ namespace NzbDrone.Core.Download.Pending return releases.Where(release => !blockedIndexers.Contains(release.IndexerId)).ToList(); } - public List GetPendingRemoteAlbums(int artistId) + public List GetPendingRemoteAlbums(int authorId) { - return IncludeRemoteAlbums(_repository.AllByArtistId(artistId)).Select(v => v.RemoteAlbum).ToList(); + return IncludeRemoteAlbums(_repository.AllByAuthorId(authorId)).Select(v => v.RemoteAlbum).ToList(); } public List GetPendingQueue() @@ -231,7 +231,7 @@ namespace NzbDrone.Core.Download.Pending public void RemovePendingQueueItems(int queueId) { var targetItem = FindPendingRelease(queueId); - var artistReleases = _repository.AllByArtistId(targetItem.ArtistId); + var artistReleases = _repository.AllByAuthorId(targetItem.AuthorId); var releasesToRemove = artistReleases.Where( c => c.ParsedAlbumInfo.AlbumTitle == targetItem.ParsedAlbumInfo.AlbumTitle); @@ -239,12 +239,12 @@ namespace NzbDrone.Core.Download.Pending _repository.DeleteMany(releasesToRemove.Select(c => c.Id)); } - public RemoteAlbum OldestPendingRelease(int artistId, int[] albumIds) + public RemoteAlbum OldestPendingRelease(int authorId, int[] bookIds) { - var artistReleases = GetPendingReleases(artistId); + var artistReleases = GetPendingReleases(authorId); return artistReleases.Select(r => r.RemoteAlbum) - .Where(r => r.Albums.Select(e => e.Id).Intersect(albumIds).Any()) + .Where(r => r.Albums.Select(e => e.Id).Intersect(bookIds).Any()) .OrderByDescending(p => p.Release.AgeHours) .FirstOrDefault(); } @@ -254,16 +254,16 @@ namespace NzbDrone.Core.Download.Pending return IncludeRemoteAlbums(_repository.All().ToList()); } - private List GetPendingReleases(int artistId) + private List GetPendingReleases(int authorId) { - return IncludeRemoteAlbums(_repository.AllByArtistId(artistId).ToList()); + return IncludeRemoteAlbums(_repository.AllByAuthorId(authorId).ToList()); } private List IncludeRemoteAlbums(List releases, Dictionary knownRemoteAlbums = null) { var result = new List(); - var artistMap = new Dictionary(); + var artistMap = new Dictionary(); if (knownRemoteAlbums != null) { @@ -276,14 +276,14 @@ namespace NzbDrone.Core.Download.Pending } } - foreach (var artist in _artistService.GetArtists(releases.Select(v => v.ArtistId).Distinct().Where(v => !artistMap.ContainsKey(v)))) + foreach (var artist in _artistService.GetArtists(releases.Select(v => v.AuthorId).Distinct().Where(v => !artistMap.ContainsKey(v)))) { artistMap[artist.Id] = artist; } foreach (var release in releases) { - var artist = artistMap.GetValueOrDefault(release.ArtistId); + var artist = artistMap.GetValueOrDefault(release.AuthorId); // Just in case the artist was removed, but wasn't cleaned up yet (housekeeper will clean it up) if (artist == null) @@ -291,7 +291,7 @@ namespace NzbDrone.Core.Download.Pending return null; } - List albums; + List albums; RemoteAlbum knownRemoteAlbum; if (knownRemoteAlbums != null && knownRemoteAlbums.TryGetValue(release.Release.Title, out knownRemoteAlbum)) @@ -321,7 +321,7 @@ namespace NzbDrone.Core.Download.Pending { _repository.Insert(new PendingRelease { - ArtistId = decision.RemoteAlbum.Artist.Id, + AuthorId = decision.RemoteAlbum.Artist.Id, ParsedAlbumInfo = decision.RemoteAlbum.ParsedAlbumInfo, Release = decision.RemoteAlbum.Release, Title = decision.RemoteAlbum.Release.Title, @@ -357,10 +357,10 @@ namespace NzbDrone.Core.Download.Pending private void RemoveGrabbed(RemoteAlbum remoteAlbum) { var pendingReleases = GetPendingReleases(remoteAlbum.Artist.Id); - var albumIds = remoteAlbum.Albums.Select(e => e.Id); + var bookIds = remoteAlbum.Albums.Select(e => e.Id); var existingReports = pendingReleases.Where(r => r.RemoteAlbum.Albums.Select(e => e.Id) - .Intersect(albumIds) + .Intersect(bookIds) .Any()) .ToList(); @@ -408,12 +408,12 @@ namespace NzbDrone.Core.Download.Pending return GetPendingReleases().First(p => p.RemoteAlbum.Albums.Any(e => queueId == GetQueueId(p, e))); } - private int GetQueueId(PendingRelease pendingRelease, Album album) + private int GetQueueId(PendingRelease pendingRelease, Book album) { return HashConverter.GetHashInt31(string.Format("pending-{0}-album{1}", pendingRelease.Id, album.Id)); } - private int PrioritizeDownloadProtocol(Artist artist, DownloadProtocol downloadProtocol) + private int PrioritizeDownloadProtocol(Author artist, DownloadProtocol downloadProtocol) { var delayProfile = _delayProfileService.BestForTags(artist.Tags); @@ -427,7 +427,7 @@ namespace NzbDrone.Core.Download.Pending public void Handle(ArtistDeletedEvent message) { - _repository.DeleteByArtistId(message.Artist.Id); + _repository.DeleteByAuthorId(message.Artist.Id); } public void Handle(AlbumGrabbedEvent message) diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs index f43bd75eb..e64f6ffeb 100644 --- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -121,12 +121,12 @@ namespace NzbDrone.Core.Download private bool IsAlbumProcessed(List decisions, DownloadDecision report) { - var albumIds = report.RemoteAlbum.Albums.Select(e => e.Id).ToList(); + var bookIds = report.RemoteAlbum.Albums.Select(e => e.Id).ToList(); return decisions.SelectMany(r => r.RemoteAlbum.Albums) .Select(e => e.Id) .ToList() - .Intersect(albumIds) + .Intersect(bookIds) .Any(); } diff --git a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs index a091211f6..017601a3f 100644 --- a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs @@ -41,24 +41,24 @@ namespace NzbDrone.Core.Download return; } - if (message.AlbumIds.Count == 1) + if (message.BookIds.Count == 1) { _logger.Debug("Failed download only contains one album, searching again"); - _commandQueueManager.Push(new AlbumSearchCommand(message.AlbumIds)); + _commandQueueManager.Push(new AlbumSearchCommand(message.BookIds)); return; } - var albumsInArtist = _albumService.GetAlbumsByArtist(message.ArtistId); + var albumsInArtist = _albumService.GetAlbumsByArtist(message.AuthorId); - if (message.AlbumIds.Count == albumsInArtist.Count) + if (message.BookIds.Count == albumsInArtist.Count) { _logger.Debug("Failed download was entire artist, searching again"); _commandQueueManager.Push(new ArtistSearchCommand { - ArtistId = message.ArtistId + AuthorId = message.AuthorId }); return; @@ -66,7 +66,7 @@ namespace NzbDrone.Core.Download _logger.Debug("Failed download contains multiple albums, searching again"); - _commandQueueManager.Push(new AlbumSearchCommand(message.AlbumIds)); + _commandQueueManager.Push(new AlbumSearchCommand(message.BookIds)); } } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs index 3a91ae657..22217d8c1 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadAlreadyImported.cs @@ -26,7 +26,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads var allAlbumsImportedInHistory = trackedDownload.RemoteAlbum.Albums.All(album => { - var lastHistoryItem = historyItems.FirstOrDefault(h => h.AlbumId == album.Id); + var lastHistoryItem = historyItems.FirstOrDefault(h => h.BookId == album.Id); if (lastHistoryItem == null) { diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index f8ee61ba4..bff07fd21 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -52,9 +52,9 @@ namespace NzbDrone.Core.Download.TrackedDownloads return _cache.Find(downloadId); } - public void UpdateAlbumCache(int albumId) + public void UpdateAlbumCache(int bookId) { - var updateCacheItems = _cache.Values.Where(x => x.RemoteAlbum != null && x.RemoteAlbum.Albums.Any(a => a.Id == albumId)).ToList(); + var updateCacheItems = _cache.Values.Where(x => x.RemoteAlbum != null && x.RemoteAlbum.Albums.Any(a => a.Id == bookId)).ToList(); foreach (var item in updateCacheItems) { @@ -161,15 +161,15 @@ namespace NzbDrone.Core.Download.TrackedDownloads // Try parsing the original source title and if that fails, try parsing it as a special // TODO: Pass the TVDB ID and TVRage IDs in as well so we have a better chance for finding the item var historyArtist = firstHistoryItem.Artist; - var historyAlbums = new List { firstHistoryItem.Album }; + var historyAlbums = new List { firstHistoryItem.Album }; parsedAlbumInfo = Parser.Parser.ParseAlbumTitle(firstHistoryItem.SourceTitle); if (parsedAlbumInfo != null) { trackedDownload.RemoteAlbum = _parsingService.Map(parsedAlbumInfo, - firstHistoryItem.ArtistId, - historyItems.Where(v => v.EventType == HistoryEventType.Grabbed).Select(h => h.AlbumId) + firstHistoryItem.AuthorId, + historyItems.Where(v => v.EventType == HistoryEventType.Grabbed).Select(h => h.BookId) .Distinct()); } else @@ -182,8 +182,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads if (parsedAlbumInfo != null) { trackedDownload.RemoteAlbum = _parsingService.Map(parsedAlbumInfo, - firstHistoryItem.ArtistId, - historyItems.Where(v => v.EventType == HistoryEventType.Grabbed).Select(h => h.AlbumId) + firstHistoryItem.AuthorId, + historyItems.Where(v => v.EventType == HistoryEventType.Grabbed).Select(h => h.BookId) .Distinct()); } } diff --git a/src/NzbDrone.Core/Exceptions/AlbumNotFoundException.cs b/src/NzbDrone.Core/Exceptions/AlbumNotFoundException.cs index d07d6adb9..a580acded 100644 --- a/src/NzbDrone.Core/Exceptions/AlbumNotFoundException.cs +++ b/src/NzbDrone.Core/Exceptions/AlbumNotFoundException.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Exceptions public string MusicBrainzId { get; set; } public AlbumNotFoundException(string musicbrainzId) - : base(string.Format("Album with MusicBrainz {0} was not found, it may have been removed from MusicBrainz.", musicbrainzId)) + : base(string.Format("Album with id {0} was not found, it may have been removed from metadata server.", musicbrainzId)) { MusicBrainzId = musicbrainzId; } diff --git a/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs b/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs index fc1e69078..0f8af670a 100644 --- a/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs +++ b/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Exceptions public string MusicBrainzId { get; set; } public ArtistNotFoundException(string musicbrainzId) - : base(string.Format("Artist with MusicBrainz {0} was not found, it may have been removed from MusicBrainz.", musicbrainzId)) + : base(string.Format("Artist with id {0} was not found, it may have been removed from the metadata server.", musicbrainzId)) { MusicBrainzId = musicbrainzId; } diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index 1b3969d31..f16fa7abb 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Extras { public interface IExtraService { - void ImportTrack(LocalTrack localTrack, TrackFile trackFile, bool isReadOnly); + void ImportTrack(LocalTrack localTrack, BookFile trackFile, bool isReadOnly); } public class ExtraService : IExtraService, @@ -28,7 +28,6 @@ namespace NzbDrone.Core.Extras { private readonly IMediaFileService _mediaFileService; private readonly IAlbumService _albumService; - private readonly ITrackService _trackService; private readonly IDiskProvider _diskProvider; private readonly IConfigService _configService; private readonly List _extraFileManagers; @@ -36,7 +35,6 @@ namespace NzbDrone.Core.Extras public ExtraService(IMediaFileService mediaFileService, IAlbumService albumService, - ITrackService trackService, IDiskProvider diskProvider, IConfigService configService, List extraFileManagers, @@ -44,21 +42,20 @@ namespace NzbDrone.Core.Extras { _mediaFileService = mediaFileService; _albumService = albumService; - _trackService = trackService; _diskProvider = diskProvider; _configService = configService; _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); _logger = logger; } - public void ImportTrack(LocalTrack localTrack, TrackFile trackFile, bool isReadOnly) + public void ImportTrack(LocalTrack localTrack, BookFile trackFile, bool isReadOnly) { ImportExtraFiles(localTrack, trackFile, isReadOnly); CreateAfterImport(localTrack.Artist, trackFile); } - public void ImportExtraFiles(LocalTrack localTrack, TrackFile trackFile, bool isReadOnly) + public void ImportExtraFiles(LocalTrack localTrack, BookFile trackFile, bool isReadOnly) { if (!_configService.ImportExtraFiles) { @@ -123,7 +120,7 @@ namespace NzbDrone.Core.Extras } } - private void CreateAfterImport(Artist artist, TrackFile trackFile) + private void CreateAfterImport(Author artist, BookFile trackFile) { foreach (var extraFileManager in _extraFileManagers) { @@ -146,7 +143,7 @@ namespace NzbDrone.Core.Extras public void Handle(TrackFolderCreatedEvent message) { var artist = message.Artist; - var album = _albumService.GetAlbum(message.TrackFile.AlbumId); + var album = _albumService.GetAlbum(message.TrackFile.BookId); foreach (var extraFileManager in _extraFileManagers) { @@ -165,18 +162,9 @@ namespace NzbDrone.Core.Extras } } - private List GetTrackFiles(int artistId) + private List GetTrackFiles(int authorId) { - var trackFiles = _mediaFileService.GetFilesByArtist(artistId); - var tracks = _trackService.GetTracksByArtist(artistId); - - foreach (var trackFile in trackFiles) - { - var localTrackFile = trackFile; - trackFile.Tracks = tracks.Where(e => e.TrackFileId == localTrackFile.Id).ToList(); - } - - return trackFiles; + return _mediaFileService.GetFilesByArtist(authorId); } } } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFile.cs b/src/NzbDrone.Core/Extras/Files/ExtraFile.cs index 1e3c2b8bf..f3737e572 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFile.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFile.cs @@ -5,9 +5,9 @@ namespace NzbDrone.Core.Extras.Files { public abstract class ExtraFile : ModelBase { - public int ArtistId { get; set; } + public int AuthorId { get; set; } public int? TrackFileId { get; set; } - public int? AlbumId { get; set; } + public int? BookId { get; set; } public string RelativePath { get; set; } public DateTime Added { get; set; } public DateTime LastUpdated { get; set; } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs index c618557d9..7da4509ca 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs @@ -14,11 +14,11 @@ namespace NzbDrone.Core.Extras.Files public interface IManageExtraFiles { int Order { get; } - IEnumerable CreateAfterArtistScan(Artist artist, List trackFiles); - IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile); - IEnumerable CreateAfterTrackImport(Artist artist, Album album, string artistFolder, string albumFolder); - IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles); - ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly); + IEnumerable CreateAfterArtistScan(Author artist, List trackFiles); + IEnumerable CreateAfterTrackImport(Author artist, BookFile trackFile); + IEnumerable CreateAfterTrackImport(Author artist, Book album, string artistFolder, string albumFolder); + IEnumerable MoveFilesAfterRename(Author artist, List trackFiles); + ExtraFile Import(Author artist, BookFile trackFile, string path, string extension, bool readOnly); } public abstract class ExtraFileManager : IManageExtraFiles @@ -41,13 +41,13 @@ namespace NzbDrone.Core.Extras.Files } public abstract int Order { get; } - public abstract IEnumerable CreateAfterArtistScan(Artist artist, List trackFiles); - public abstract IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile); - public abstract IEnumerable CreateAfterTrackImport(Artist artist, Album album, string artistFolder, string albumFolder); - public abstract IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles); - public abstract ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly); + public abstract IEnumerable CreateAfterArtistScan(Author artist, List trackFiles); + public abstract IEnumerable CreateAfterTrackImport(Author artist, BookFile trackFile); + public abstract IEnumerable CreateAfterTrackImport(Author artist, Book album, string artistFolder, string albumFolder); + public abstract IEnumerable MoveFilesAfterRename(Author artist, List trackFiles); + public abstract ExtraFile Import(Author artist, BookFile trackFile, string path, string extension, bool readOnly); - protected TExtraFile ImportFile(Artist artist, TrackFile trackFile, string path, bool readOnly, string extension, string fileNameSuffix = null) + protected TExtraFile ImportFile(Author artist, BookFile trackFile, string path, bool readOnly, string extension, string fileNameSuffix = null) { var newFolder = Path.GetDirectoryName(trackFile.Path); var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.Path)); @@ -71,15 +71,15 @@ namespace NzbDrone.Core.Extras.Files return new TExtraFile { - ArtistId = artist.Id, - AlbumId = trackFile.AlbumId, + AuthorId = artist.Id, + BookId = trackFile.BookId, TrackFileId = trackFile.Id, RelativePath = artist.Path.GetRelativePath(newFileName), Extension = extension }; } - protected TExtraFile MoveFile(Artist artist, TrackFile trackFile, TExtraFile extraFile, string fileNameSuffix = null) + protected TExtraFile MoveFile(Author artist, BookFile trackFile, TExtraFile extraFile, string fileNameSuffix = null) { var newFolder = Path.GetDirectoryName(trackFile.Path); var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.Path)); diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs index ff5113253..2c0eeeb69 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs @@ -8,11 +8,11 @@ namespace NzbDrone.Core.Extras.Files public interface IExtraFileRepository : IBasicRepository where TExtraFile : ExtraFile, new() { - void DeleteForArtist(int artistId); - void DeleteForAlbum(int artistId, int albumId); + void DeleteForArtist(int authorId); + void DeleteForAlbum(int authorId, int bookId); void DeleteForTrackFile(int trackFileId); - List GetFilesByArtist(int artistId); - List GetFilesByAlbum(int artistId, int albumId); + List GetFilesByArtist(int authorId); + List GetFilesByAlbum(int authorId, int bookId); List GetFilesByTrackFile(int trackFileId); TExtraFile FindByPath(string path); } @@ -25,14 +25,14 @@ namespace NzbDrone.Core.Extras.Files { } - public void DeleteForArtist(int artistId) + public void DeleteForArtist(int authorId) { - Delete(c => c.ArtistId == artistId); + Delete(c => c.AuthorId == authorId); } - public void DeleteForAlbum(int artistId, int albumId) + public void DeleteForAlbum(int authorId, int bookId) { - Delete(c => c.ArtistId == artistId && c.AlbumId == albumId); + Delete(c => c.AuthorId == authorId && c.BookId == bookId); } public void DeleteForTrackFile(int trackFileId) @@ -40,14 +40,14 @@ namespace NzbDrone.Core.Extras.Files Delete(c => c.TrackFileId == trackFileId); } - public List GetFilesByArtist(int artistId) + public List GetFilesByArtist(int authorId) { - return Query(c => c.ArtistId == artistId); + return Query(c => c.AuthorId == authorId); } - public List GetFilesByAlbum(int artistId, int albumId) + public List GetFilesByAlbum(int authorId, int bookId) { - return Query(c => c.ArtistId == artistId && c.AlbumId == albumId); + return Query(c => c.AuthorId == authorId && c.BookId == bookId); } public List GetFilesByTrackFile(int trackFileId) diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs index f4a8dc8f2..aaecf56aa 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.Extras.Files public interface IExtraFileService where TExtraFile : ExtraFile, new() { - List GetFilesByArtist(int artistId); + List GetFilesByArtist(int authorId); List GetFilesByTrackFile(int trackFileId); TExtraFile FindByPath(string path); void Upsert(TExtraFile extraFile); @@ -49,9 +49,9 @@ namespace NzbDrone.Core.Extras.Files _logger = logger; } - public List GetFilesByArtist(int artistId) + public List GetFilesByArtist(int authorId) { - return _repository.GetFilesByArtist(artistId); + return _repository.GetFilesByArtist(authorId); } public List GetFilesByTrackFile(int trackFileId) diff --git a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs index cb5a7dcff..458b0282e 100644 --- a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs +++ b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs @@ -7,6 +7,6 @@ namespace NzbDrone.Core.Extras public interface IImportExistingExtraFiles { int Order { get; } - IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles); + IEnumerable ProcessFiles(Author artist, List filesOnDisk, List importedFiles); } } diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs index c232259c4..a6801fe7a 100644 --- a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -19,9 +19,9 @@ namespace NzbDrone.Core.Extras } public abstract int Order { get; } - public abstract IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles); + public abstract IEnumerable ProcessFiles(Author artist, List filesOnDisk, List importedFiles); - public virtual ImportExistingExtraFileFilterResult FilterAndClean(Artist artist, List filesOnDisk, List importedFiles) + public virtual ImportExistingExtraFileFilterResult FilterAndClean(Author artist, List filesOnDisk, List importedFiles) { var artistFiles = _extraFileService.GetFilesByArtist(artist.Id); @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Extras return Filter(artist, filesOnDisk, importedFiles, artistFiles); } - private ImportExistingExtraFileFilterResult Filter(Artist artist, List filesOnDisk, List importedFiles, List artistFiles) + private ImportExistingExtraFileFilterResult Filter(Author artist, List filesOnDisk, List importedFiles, List artistFiles) { var previouslyImported = artistFiles.IntersectBy(s => Path.Combine(artist.Path, s.RelativePath), filesOnDisk, f => f, PathEqualityComparer.Instance).ToList(); var filteredFiles = filesOnDisk.Except(previouslyImported.Select(f => Path.Combine(artist.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance) @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Extras return new ImportExistingExtraFileFilterResult(previouslyImported, filteredFiles); } - private void Clean(Artist artist, List filesOnDisk, List importedFiles, List artistFiles) + private void Clean(Author artist, List filesOnDisk, List importedFiles, List artistFiles) { var alreadyImportedFileIds = artistFiles.IntersectBy(f => Path.Combine(artist.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance) .Select(f => f.Id); diff --git a/src/NzbDrone.Core/Extras/Lyrics/ExistingLyricImporter.cs b/src/NzbDrone.Core/Extras/Lyrics/ExistingLyricImporter.cs deleted file mode 100644 index fae0518b4..000000000 --- a/src/NzbDrone.Core/Extras/Lyrics/ExistingLyricImporter.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.MediaFiles.TrackImport.Aggregation; -using NzbDrone.Core.Music; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Extras.Lyrics -{ - public class ExistingLyricImporter : ImportExistingExtraFilesBase - { - private readonly IExtraFileService _lyricFileService; - private readonly IAugmentingService _augmentingService; - private readonly Logger _logger; - - public ExistingLyricImporter(IExtraFileService lyricFileService, - IAugmentingService augmentingService, - Logger logger) - : base(lyricFileService) - { - _lyricFileService = lyricFileService; - _augmentingService = augmentingService; - _logger = logger; - } - - public override int Order => 1; - - public override IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles) - { - _logger.Debug("Looking for existing lyrics files in {0}", artist.Path); - - var subtitleFiles = new List(); - var filterResult = FilterAndClean(artist, filesOnDisk, importedFiles); - - foreach (var possibleLyricFile in filterResult.FilesOnDisk) - { - var extension = Path.GetExtension(possibleLyricFile); - - if (LyricFileExtensions.Extensions.Contains(extension)) - { - var localTrack = new LocalTrack - { - FileTrackInfo = Parser.Parser.ParseMusicPath(possibleLyricFile), - Artist = artist, - Path = possibleLyricFile - }; - - try - { - _augmentingService.Augment(localTrack, false); - } - catch (AugmentingFailedException) - { - _logger.Debug("Unable to parse lyric file: {0}", possibleLyricFile); - continue; - } - - if (localTrack.Tracks.Empty()) - { - _logger.Debug("Cannot find related tracks for: {0}", possibleLyricFile); - continue; - } - - if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1) - { - _logger.Debug("Lyric file: {0} does not match existing files.", possibleLyricFile); - continue; - } - - var subtitleFile = new LyricFile - { - ArtistId = artist.Id, - AlbumId = localTrack.Album.Id, - TrackFileId = localTrack.Tracks.First().TrackFileId, - RelativePath = artist.Path.GetRelativePath(possibleLyricFile), - Extension = extension - }; - - subtitleFiles.Add(subtitleFile); - } - } - - _logger.Info("Found {0} existing lyric files", subtitleFiles.Count); - _lyricFileService.Upsert(subtitleFiles); - - // Return files that were just imported along with files that were - // previously imported so previously imported files aren't imported twice - return subtitleFiles.Concat(filterResult.PreviouslyImported); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Lyrics/ImportedLyricFiles.cs b/src/NzbDrone.Core/Extras/Lyrics/ImportedLyricFiles.cs deleted file mode 100644 index abbaa4c74..000000000 --- a/src/NzbDrone.Core/Extras/Lyrics/ImportedLyricFiles.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Extras.Files; - -namespace NzbDrone.Core.Extras.Lyrics -{ - public class ImportedLyricFiles - { - public List SourceFiles { get; set; } - public List LyricFiles { get; set; } - - public ImportedLyricFiles() - { - SourceFiles = new List(); - LyricFiles = new List(); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricFile.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricFile.cs deleted file mode 100644 index 8634b167d..000000000 --- a/src/NzbDrone.Core/Extras/Lyrics/LyricFile.cs +++ /dev/null @@ -1,8 +0,0 @@ -using NzbDrone.Core.Extras.Files; - -namespace NzbDrone.Core.Extras.Lyrics -{ - public class LyricFile : ExtraFile - { - } -} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricFileExtensions.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricFileExtensions.cs deleted file mode 100644 index 1d47fc0b1..000000000 --- a/src/NzbDrone.Core/Extras/Lyrics/LyricFileExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace NzbDrone.Core.Extras.Lyrics -{ - public static class LyricFileExtensions - { - private static HashSet _fileExtensions; - - static LyricFileExtensions() - { - _fileExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) - { - ".lrc", - ".txt", - ".utf", - ".utf8", - ".utf-8" - }; - } - - public static HashSet Extensions => _fileExtensions; - } -} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricFileRepository.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricFileRepository.cs deleted file mode 100644 index 60fe50f45..000000000 --- a/src/NzbDrone.Core/Extras/Lyrics/LyricFileRepository.cs +++ /dev/null @@ -1,18 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Extras.Lyrics -{ - public interface ILyricFileRepository : IExtraFileRepository - { - } - - public class LyricFileRepository : ExtraFileRepository, ILyricFileRepository - { - public LyricFileRepository(IMainDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - } -} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricFileService.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricFileService.cs deleted file mode 100644 index 4d2935ce5..000000000 --- a/src/NzbDrone.Core/Extras/Lyrics/LyricFileService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Extras.Lyrics -{ - public interface ILyricFileService : IExtraFileService - { - } - - public class LyricFileService : ExtraFileService, ILyricFileService - { - public LyricFileService(IExtraFileRepository repository, IArtistService artistService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) - : base(repository, artistService, diskProvider, recycleBinProvider, logger) - { - } - } -} diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs deleted file mode 100644 index ae4a32e0d..000000000 --- a/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Extras.Lyrics -{ - public class LyricService : ExtraFileManager - { - private readonly ILyricFileService _lyricFileService; - private readonly Logger _logger; - - public LyricService(IConfigService configService, - IDiskProvider diskProvider, - IDiskTransferService diskTransferService, - ILyricFileService lyricFileService, - Logger logger) - : base(configService, diskProvider, diskTransferService, logger) - { - _lyricFileService = lyricFileService; - _logger = logger; - } - - public override int Order => 1; - - public override IEnumerable CreateAfterArtistScan(Artist artist, List trackFiles) - { - return Enumerable.Empty(); - } - - public override IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile) - { - return Enumerable.Empty(); - } - - public override IEnumerable CreateAfterTrackImport(Artist artist, Album album, string artistFolder, string albumFolder) - { - return Enumerable.Empty(); - } - - public override IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles) - { - var subtitleFiles = _lyricFileService.GetFilesByArtist(artist.Id); - - var movedFiles = new List(); - - foreach (var trackFile in trackFiles) - { - var groupedExtraFilesForTrackFile = subtitleFiles.Where(m => m.TrackFileId == trackFile.Id) - .GroupBy(s => s.Extension).ToList(); - - foreach (var group in groupedExtraFilesForTrackFile) - { - var groupCount = group.Count(); - var copy = 1; - - if (groupCount > 1) - { - _logger.Warn("Multiple lyric files found with the same extension for {0}", trackFile.Path); - } - - foreach (var subtitleFile in group) - { - var suffix = GetSuffix(copy, groupCount > 1); - movedFiles.AddIfNotNull(MoveFile(artist, trackFile, subtitleFile, suffix)); - - copy++; - } - } - } - - _lyricFileService.Upsert(movedFiles); - - return movedFiles; - } - - public override ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly) - { - if (LyricFileExtensions.Extensions.Contains(Path.GetExtension(path))) - { - var suffix = GetSuffix(1, false); - var subtitleFile = ImportFile(artist, trackFile, path, readOnly, extension, suffix); - - _lyricFileService.Upsert(subtitleFile); - - return subtitleFile; - } - - return null; - } - - private string GetSuffix(int copy, bool multipleCopies = false) - { - var suffixBuilder = new StringBuilder(); - - if (multipleCopies) - { - suffixBuilder.Append("."); - suffixBuilder.Append(copy); - } - - return suffixBuilder.ToString(); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs deleted file mode 100644 index fb64e6d6b..000000000 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; -using System.Xml.Linq; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Extras.Metadata.Files; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox -{ - public class RoksboxMetadata : MetadataBase - { - private readonly IMapCoversToLocal _mediaCoverService; - private readonly IDiskProvider _diskProvider; - private readonly Logger _logger; - - public RoksboxMetadata(IMapCoversToLocal mediaCoverService, - IDiskProvider diskProvider, - Logger logger) - { - _mediaCoverService = mediaCoverService; - _diskProvider = diskProvider; - _logger = logger; - } - - private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?\d+))|(?specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public override string Name => "Roksbox"; - - public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) - { - var trackFilePath = trackFile.Path; - - if (metadataFile.Type == MetadataType.TrackMetadata) - { - return GetTrackMetadataFilename(trackFilePath); - } - - _logger.Debug("Unknown track file metadata: {0}", metadataFile.RelativePath); - return Path.Combine(artist.Path, metadataFile.RelativePath); - } - - public override MetadataFile FindMetadataFile(Artist artist, string path) - { - var filename = Path.GetFileName(path); - - if (filename == null) - { - return null; - } - - var parentdir = Directory.GetParent(path); - - var metadata = new MetadataFile - { - ArtistId = artist.Id, - Consumer = GetType().Name, - RelativePath = artist.Path.GetRelativePath(path) - }; - - //Series and season images are both named folder.jpg, only season ones sit in season folders - if (Path.GetFileNameWithoutExtension(filename).Equals(parentdir.Name, StringComparison.InvariantCultureIgnoreCase)) - { - var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); - - if (seasonMatch.Success) - { - metadata.Type = MetadataType.AlbumImage; - - if (seasonMatch.Groups["specials"].Success) - { - metadata.AlbumId = 0; - } - else - { - metadata.AlbumId = Convert.ToInt32(seasonMatch.Groups["season"].Value); - } - - return metadata; - } - - metadata.Type = MetadataType.ArtistImage; - return metadata; - } - - var parseResult = Parser.Parser.ParseMusicTitle(filename); - - if (parseResult != null) - { - var extension = Path.GetExtension(filename).ToLowerInvariant(); - - if (extension == ".xml") - { - metadata.Type = MetadataType.TrackMetadata; - return metadata; - } - } - - return null; - } - - public override MetadataFileResult ArtistMetadata(Artist artist) - { - //Artist metadata is not supported - return null; - } - - public override MetadataFileResult AlbumMetadata(Artist artist, Album album, string albumPath) - { - return null; - } - - public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile) - { - if (!Settings.TrackMetadata) - { - return null; - } - - _logger.Debug("Generating Track Metadata for: {0}", trackFile.Path); - - var xmlResult = string.Empty; - foreach (var track in trackFile.Tracks.Value) - { - var sb = new StringBuilder(); - var xws = new XmlWriterSettings(); - xws.OmitXmlDeclaration = true; - xws.Indent = false; - - using (var xw = XmlWriter.Create(sb, xws)) - { - var doc = new XDocument(); - - var details = new XElement("song"); - details.Add(new XElement("title", track.Title)); - details.Add(new XElement("performingartist", artist.Name)); - - doc.Add(details); - doc.Save(xw); - - xmlResult += doc.ToString(); - xmlResult += Environment.NewLine; - } - } - - return new MetadataFileResult(GetTrackMetadataFilename(artist.Path.GetRelativePath(trackFile.Path)), xmlResult.Trim(Environment.NewLine.ToCharArray())); - } - - public override List ArtistImages(Artist artist) - { - if (!Settings.ArtistImages) - { - return new List(); - } - - var image = artist.Metadata.Value.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? artist.Metadata.Value.Images.FirstOrDefault(); - if (image == null) - { - _logger.Trace("Failed to find suitable Artist image for artist {0}.", artist.Name); - return new List(); - } - - var source = _mediaCoverService.GetCoverPath(artist.Id, MediaCoverEntity.Artist, image.CoverType, image.Extension); - var destination = Path.GetFileName(artist.Path) + Path.GetExtension(source); - - return new List { new ImageFileResult(destination, source) }; - } - - public override List AlbumImages(Artist artist, Album album, string albumFolder) - { - return new List(); - } - - public override List TrackImages(Artist artist, TrackFile trackFile) - { - return new List(); - } - - private string GetTrackMetadataFilename(string trackFilePath) - { - return Path.ChangeExtension(trackFilePath, "xml"); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs deleted file mode 100644 index d0213c72a..000000000 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs +++ /dev/null @@ -1,39 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox -{ - public class RoksboxSettingsValidator : AbstractValidator - { - } - - public class RoksboxMetadataSettings : IProviderConfig - { - private static readonly RoksboxSettingsValidator Validator = new RoksboxSettingsValidator(); - - public RoksboxMetadataSettings() - { - TrackMetadata = true; - ArtistImages = true; - AlbumImages = true; - } - - [FieldDefinition(0, Label = "Track Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "Album\\filename.xml")] - public bool TrackMetadata { get; set; } - - [FieldDefinition(1, Label = "Artist Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Artist Title.jpg")] - public bool ArtistImages { get; set; } - - [FieldDefinition(2, Label = "Album Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Album Title.jpg")] - public bool AlbumImages { get; set; } - - public bool IsValid => true; - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs deleted file mode 100644 index 609f8b6dd..000000000 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Xml; -using System.Xml.Linq; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Extras.Metadata.Files; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv -{ - public class WdtvMetadata : MetadataBase - { - private readonly IMapCoversToLocal _mediaCoverService; - private readonly IDiskProvider _diskProvider; - private readonly Logger _logger; - - public WdtvMetadata(IMapCoversToLocal mediaCoverService, - IDiskProvider diskProvider, - Logger logger) - { - _mediaCoverService = mediaCoverService; - _diskProvider = diskProvider; - _logger = logger; - } - - public override string Name => "WDTV"; - - public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) - { - var trackFilePath = trackFile.Path; - - if (metadataFile.Type == MetadataType.TrackMetadata) - { - return GetTrackMetadataFilename(trackFilePath); - } - - _logger.Debug("Unknown track file metadata: {0}", metadataFile.RelativePath); - return Path.Combine(artist.Path, metadataFile.RelativePath); - } - - public override MetadataFile FindMetadataFile(Artist artist, string path) - { - var filename = Path.GetFileName(path); - - if (filename == null) - { - return null; - } - - var metadata = new MetadataFile - { - ArtistId = artist.Id, - Consumer = GetType().Name, - RelativePath = artist.Path.GetRelativePath(path) - }; - - var parseResult = Parser.Parser.ParseMusicTitle(filename); - - if (parseResult != null) - { - switch (Path.GetExtension(filename).ToLowerInvariant()) - { - case ".xml": - metadata.Type = MetadataType.TrackMetadata; - return metadata; - } - } - - return null; - } - - public override MetadataFileResult ArtistMetadata(Artist artist) - { - //Artist metadata is not supported - return null; - } - - public override MetadataFileResult AlbumMetadata(Artist artist, Album album, string albumPath) - { - return null; - } - - public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile) - { - if (!Settings.TrackMetadata) - { - return null; - } - - _logger.Debug("Generating Track Metadata for: {0}", trackFile.Path); - - var xmlResult = string.Empty; - foreach (var track in trackFile.Tracks.Value) - { - var sb = new StringBuilder(); - var xws = new XmlWriterSettings(); - xws.OmitXmlDeclaration = true; - xws.Indent = false; - - using (var xw = XmlWriter.Create(sb, xws)) - { - var doc = new XDocument(); - - var details = new XElement("details"); - details.Add(new XElement("id", artist.Id)); - details.Add(new XElement("title", string.Format("{0} - {1} - {2}", artist.Name, track.TrackNumber, track.Title))); - details.Add(new XElement("artist_name", artist.Metadata.Value.Name)); - details.Add(new XElement("track_name", track.Title)); - details.Add(new XElement("track_number", track.AbsoluteTrackNumber.ToString("00"))); - details.Add(new XElement("member", string.Join(" / ", artist.Metadata.Value.Members.ConvertAll(c => c.Name + " - " + c.Instrument)))); - - doc.Add(details); - doc.Save(xw); - - xmlResult += doc.ToString(); - xmlResult += Environment.NewLine; - } - } - - var filename = GetTrackMetadataFilename(artist.Path.GetRelativePath(trackFile.Path)); - - return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); - } - - public override List ArtistImages(Artist artist) - { - return new List(); - } - - public override List AlbumImages(Artist artist, Album album, string albumFolder) - { - return new List(); - } - - public override List TrackImages(Artist artist, TrackFile trackFile) - { - return new List(); - } - - private string GetTrackMetadataFilename(string trackFilePath) - { - return Path.ChangeExtension(trackFilePath, "xml"); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs deleted file mode 100644 index 4045486f5..000000000 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs +++ /dev/null @@ -1,31 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv -{ - public class WdtvSettingsValidator : AbstractValidator - { - } - - public class WdtvMetadataSettings : IProviderConfig - { - private static readonly WdtvSettingsValidator Validator = new WdtvSettingsValidator(); - - public WdtvMetadataSettings() - { - TrackMetadata = true; - } - - [FieldDefinition(0, Label = "Track Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata)] - public bool TrackMetadata { get; set; } - - public bool IsValid => true; - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs deleted file mode 100644 index d48f6a141..000000000 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; -using System.Xml.Linq; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Extras.Metadata.Files; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc -{ - public class XbmcMetadata : MetadataBase - { - private readonly Logger _logger; - private readonly IMapCoversToLocal _mediaCoverService; - private readonly IDetectXbmcNfo _detectNfo; - - public XbmcMetadata(IDetectXbmcNfo detectNfo, - IMapCoversToLocal mediaCoverService, - Logger logger) - { - _logger = logger; - _mediaCoverService = mediaCoverService; - _detectNfo = detectNfo; - } - - private static readonly Regex ArtistImagesRegex = new Regex(@"^(?folder|banner|fanart|logo)\.(?:png|jpg|jpeg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex AlbumImagesRegex = new Regex(@"^(?cover|disc)\.(?:png|jpg|jpeg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public override string Name => "Kodi (XBMC) / Emby"; - - public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) - { - var trackFilePath = trackFile.Path; - - if (metadataFile.Type == MetadataType.TrackMetadata) - { - return GetTrackMetadataFilename(trackFilePath); - } - - _logger.Debug("Unknown track file metadata: {0}", metadataFile.RelativePath); - return Path.Combine(artist.Path, metadataFile.RelativePath); - } - - public override MetadataFile FindMetadataFile(Artist artist, string path) - { - var filename = Path.GetFileName(path); - - if (filename == null) - { - return null; - } - - var metadata = new MetadataFile - { - ArtistId = artist.Id, - Consumer = GetType().Name, - RelativePath = artist.Path.GetRelativePath(path) - }; - - if (ArtistImagesRegex.IsMatch(filename)) - { - metadata.Type = MetadataType.ArtistImage; - return metadata; - } - - var albumMatch = AlbumImagesRegex.Match(filename); - - if (albumMatch.Success) - { - metadata.Type = MetadataType.AlbumImage; - return metadata; - } - - var isXbmcNfoFile = _detectNfo.IsXbmcNfoFile(path); - - if (filename.Equals("artist.nfo", StringComparison.OrdinalIgnoreCase) && - isXbmcNfoFile) - { - metadata.Type = MetadataType.ArtistMetadata; - return metadata; - } - - if (filename.Equals("album.nfo", StringComparison.OrdinalIgnoreCase) && - isXbmcNfoFile) - { - metadata.Type = MetadataType.AlbumMetadata; - return metadata; - } - - return null; - } - - public override MetadataFileResult ArtistMetadata(Artist artist) - { - if (!Settings.ArtistMetadata) - { - return null; - } - - _logger.Debug("Generating artist.nfo for: {0}", artist.Name); - var sb = new StringBuilder(); - var xws = new XmlWriterSettings(); - xws.OmitXmlDeclaration = true; - xws.Indent = false; - - using (var xw = XmlWriter.Create(sb, xws)) - { - var artistElement = new XElement("artist"); - - artistElement.Add(new XElement("title", artist.Name)); - - if (artist.Metadata.Value.Ratings != null && artist.Metadata.Value.Ratings.Votes > 0) - { - artistElement.Add(new XElement("rating", artist.Metadata.Value.Ratings.Value)); - } - - artistElement.Add(new XElement("musicbrainzartistid", artist.Metadata.Value.ForeignArtistId)); - artistElement.Add(new XElement("biography", artist.Metadata.Value.Overview)); - artistElement.Add(new XElement("outline", artist.Metadata.Value.Overview)); - - var doc = new XDocument(artistElement); - doc.Save(xw); - - _logger.Debug("Saving artist.nfo for {0}", artist.Metadata.Value.Name); - - return new MetadataFileResult("artist.nfo", doc.ToString()); - } - } - - public override MetadataFileResult AlbumMetadata(Artist artist, Album album, string albumPath) - { - if (!Settings.AlbumMetadata) - { - return null; - } - - _logger.Debug("Generating album.nfo for: {0}", album.Title); - var sb = new StringBuilder(); - var xws = new XmlWriterSettings(); - xws.OmitXmlDeclaration = true; - xws.Indent = false; - - using (var xw = XmlWriter.Create(sb, xws)) - { - var albumElement = new XElement("album"); - - albumElement.Add(new XElement("title", album.Title)); - - if (album.Ratings != null && album.Ratings.Votes > 0) - { - albumElement.Add(new XElement("rating", album.Ratings.Value)); - } - - albumElement.Add(new XElement("musicbrainzalbumid", album.ForeignAlbumId)); - albumElement.Add(new XElement("artistdesc", artist.Metadata.Value.Overview)); - albumElement.Add(new XElement("releasedate", album.ReleaseDate.Value.ToShortDateString())); - - var doc = new XDocument(albumElement); - doc.Save(xw); - - _logger.Debug("Saving album.nfo for {0}", album.Title); - - var fileName = Path.Combine(albumPath, "album.nfo"); - - return new MetadataFileResult(fileName, doc.ToString()); - } - } - - public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile) - { - return null; - } - - public override List ArtistImages(Artist artist) - { - if (!Settings.ArtistImages) - { - return new List(); - } - - return ProcessArtistImages(artist).ToList(); - } - - public override List AlbumImages(Artist artist, Album album, string albumPath) - { - if (!Settings.AlbumImages) - { - return new List(); - } - - return ProcessAlbumImages(artist, album, albumPath).ToList(); - } - - public override List TrackImages(Artist artist, TrackFile trackFile) - { - return new List(); - } - - private IEnumerable ProcessArtistImages(Artist artist) - { - foreach (var image in artist.Metadata.Value.Images) - { - var source = _mediaCoverService.GetCoverPath(artist.Id, MediaCoverEntity.Artist, image.CoverType, image.Extension); - var destination = image.CoverType.ToString().ToLowerInvariant() + image.Extension; - if (image.CoverType == MediaCoverTypes.Poster) - { - destination = "folder" + image.Extension; - } - - yield return new ImageFileResult(destination, source); - } - } - - private IEnumerable ProcessAlbumImages(Artist artist, Album album, string albumPath) - { - foreach (var image in album.Images) - { - // TODO: Make Source fallback to URL if local does not exist - // var source = _mediaCoverService.GetCoverPath(album.ArtistId, image.CoverType, null, album.Id); - string filename; - - switch (image.CoverType) - { - case MediaCoverTypes.Cover: - filename = "folder"; - break; - case MediaCoverTypes.Disc: - filename = "discart"; - break; - default: - continue; - } - - var destination = Path.Combine(albumPath, filename + image.Extension); - - yield return new ImageFileResult(destination, image.Url); - } - } - - private string GetTrackMetadataFilename(string trackFilePath) - { - return Path.ChangeExtension(trackFilePath, "nfo"); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs deleted file mode 100644 index 375384e19..000000000 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs +++ /dev/null @@ -1,43 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc -{ - public class XbmcSettingsValidator : AbstractValidator - { - } - - public class XbmcMetadataSettings : IProviderConfig - { - private static readonly XbmcSettingsValidator Validator = new XbmcSettingsValidator(); - - public XbmcMetadataSettings() - { - ArtistMetadata = true; - AlbumMetadata = true; - ArtistImages = true; - AlbumImages = true; - } - - [FieldDefinition(0, Label = "Artist Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "artist.nfo")] - public bool ArtistMetadata { get; set; } - - [FieldDefinition(1, Label = "Album Metadata", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "album.nfo")] - public bool AlbumMetadata { get; set; } - - [FieldDefinition(3, Label = "Artist Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image)] - public bool ArtistImages { get; set; } - - [FieldDefinition(4, Label = "Album Images", Type = FieldType.Checkbox, Section = MetadataSectionType.Image)] - public bool AlbumImages { get; set; } - - public bool IsValid => true; - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs deleted file mode 100644 index 4132602ec..000000000 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcNfoDetector.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text.RegularExpressions; -using NzbDrone.Common.Disk; - -namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc -{ - public interface IDetectXbmcNfo - { - bool IsXbmcNfoFile(string path); - } - - public class XbmcNfoDetector : IDetectXbmcNfo - { - private readonly IDiskProvider _diskProvider; - - private readonly Regex _regex = new Regex("<(movie|tvshow|episodedetails|artist|album|musicvideo)>", RegexOptions.Compiled); - - public XbmcNfoDetector(IDiskProvider diskProvider) - { - _diskProvider = diskProvider; - } - - public bool IsXbmcNfoFile(string path) - { - // Lets make sure we're not reading huge files. - if (_diskProvider.GetFileSize(path) > 10.Megabytes()) - { - return false; - } - - // Check if it contains some of the kodi/xbmc xml tags - var content = _diskProvider.ReadAllText(path); - - return _regex.IsMatch(content); - } - } -} diff --git a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs index 61519dd46..4ec3e0581 100644 --- a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs +++ b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs @@ -4,7 +4,6 @@ using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.Extras.Lyrics; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles.TrackImport.Aggregation; using NzbDrone.Core.Music; @@ -37,7 +36,7 @@ namespace NzbDrone.Core.Extras.Metadata public override int Order => 0; - public override IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles) + public override IEnumerable ProcessFiles(Author artist, List filesOnDisk, List importedFiles) { _logger.Debug("Looking for existing metadata in {0}", artist.Path); @@ -46,12 +45,6 @@ namespace NzbDrone.Core.Extras.Metadata foreach (var possibleMetadataFile in filterResult.FilesOnDisk) { - // Don't process files that have known Subtitle file extensions (saves a bit of unecessary processing) - if (LyricFileExtensions.Extensions.Contains(Path.GetExtension(possibleMetadataFile))) - { - continue; - } - foreach (var consumer in _consumers) { var metadata = consumer.FindMetadataFile(artist, possibleMetadataFile); @@ -71,7 +64,7 @@ namespace NzbDrone.Core.Extras.Metadata continue; } - metadata.AlbumId = localAlbum.Id; + metadata.BookId = localAlbum.Id; } if (metadata.Type == MetadataType.TrackMetadata) @@ -93,19 +86,11 @@ namespace NzbDrone.Core.Extras.Metadata continue; } - if (localTrack.Tracks.Empty()) - { - _logger.Debug("Cannot find related tracks for: {0}", possibleMetadataFile); - continue; - } - - if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1) + if (localTrack.Album == null) { - _logger.Debug("Extra file: {0} does not match existing files.", possibleMetadataFile); + _logger.Debug("Cannot find related book for: {0}", possibleMetadataFile); continue; } - - metadata.TrackFileId = localTrack.Tracks.First().TrackFileId; } metadata.Extension = Path.GetExtension(possibleMetadataFile); diff --git a/src/NzbDrone.Core/Extras/Metadata/Files/CleanMetadataFileService.cs b/src/NzbDrone.Core/Extras/Metadata/Files/CleanMetadataFileService.cs index 1fe7ea41a..865ed1b89 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Files/CleanMetadataFileService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Files/CleanMetadataFileService.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Extras.Metadata.Files { public interface ICleanMetadataService { - void Clean(Artist artist); + void Clean(Author artist); } public class CleanExtraFileService : ICleanMetadataService @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras.Metadata.Files _logger = logger; } - public void Clean(Artist artist) + public void Clean(Author artist) { _logger.Debug("Cleaning missing metadata files for artist: {0}", artist.Name); diff --git a/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs index 032de483d..309dd2898 100644 --- a/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs @@ -8,14 +8,14 @@ namespace NzbDrone.Core.Extras.Metadata { public interface IMetadata : IProvider { - string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile); - string GetFilenameAfterMove(Artist artist, string albumPath, MetadataFile metadataFile); - MetadataFile FindMetadataFile(Artist artist, string path); - MetadataFileResult ArtistMetadata(Artist artist); - MetadataFileResult AlbumMetadata(Artist artist, Album album, string albumPath); - MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile); - List ArtistImages(Artist artist); - List AlbumImages(Artist artist, Album album, string albumPath); - List TrackImages(Artist artist, TrackFile trackFile); + string GetFilenameAfterMove(Author artist, BookFile trackFile, MetadataFile metadataFile); + string GetFilenameAfterMove(Author artist, string albumPath, MetadataFile metadataFile); + MetadataFile FindMetadataFile(Author artist, string path); + MetadataFileResult ArtistMetadata(Author artist); + MetadataFileResult AlbumMetadata(Author artist, Book album, string albumPath); + MetadataFileResult TrackMetadata(Author artist, BookFile trackFile); + List ArtistImages(Author artist); + List AlbumImages(Author artist, Book album, string albumPath); + List TrackImages(Author artist, BookFile trackFile); } } diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs index 55c0189cb..efa994eed 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Extras.Metadata return new ValidationResult(); } - public virtual string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) + public virtual string GetFilenameAfterMove(Author artist, BookFile trackFile, MetadataFile metadataFile) { var existingFilename = Path.Combine(artist.Path, metadataFile.RelativePath); var extension = Path.GetExtension(existingFilename).TrimStart('.'); @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Extras.Metadata return newFileName; } - public virtual string GetFilenameAfterMove(Artist artist, string albumPath, MetadataFile metadataFile) + public virtual string GetFilenameAfterMove(Author artist, string albumPath, MetadataFile metadataFile) { var existingFilename = Path.GetFileName(metadataFile.RelativePath); var newFileName = Path.Combine(artist.Path, albumPath, existingFilename); @@ -44,14 +44,14 @@ namespace NzbDrone.Core.Extras.Metadata return newFileName; } - public abstract MetadataFile FindMetadataFile(Artist artist, string path); + public abstract MetadataFile FindMetadataFile(Author artist, string path); - public abstract MetadataFileResult ArtistMetadata(Artist artist); - public abstract MetadataFileResult AlbumMetadata(Artist artist, Album album, string albumPath); - public abstract MetadataFileResult TrackMetadata(Artist artist, TrackFile trackFile); - public abstract List ArtistImages(Artist artist); - public abstract List AlbumImages(Artist artist, Album album, string albumPath); - public abstract List TrackImages(Artist artist, TrackFile trackFile); + public abstract MetadataFileResult ArtistMetadata(Author artist); + public abstract MetadataFileResult AlbumMetadata(Author artist, Book album, string albumPath); + public abstract MetadataFileResult TrackMetadata(Author artist, BookFile trackFile); + public abstract List ArtistImages(Author artist); + public abstract List AlbumImages(Author artist, Book album, string albumPath); + public abstract List TrackImages(Author artist, BookFile trackFile); public virtual object RequestAction(string action, IDictionary query) { diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs index 7432fc35b..b51f293b8 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Extras.Metadata public override int Order => 0; - public override IEnumerable CreateAfterArtistScan(Artist artist, List trackFiles) + public override IEnumerable CreateAfterArtistScan(Author artist, List trackFiles) { var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id); _cleanMetadataService.Clean(artist); @@ -83,7 +83,7 @@ namespace NzbDrone.Core.Extras.Metadata foreach (var group in albumGroups) { - var album = _albumService.GetAlbum(group.First().AlbumId); + var album = _albumService.GetAlbum(group.First().BookId); var albumFolder = group.Key; files.AddIfNotNull(ProcessAlbumMetadata(consumer, artist, album, albumFolder, consumerFiles)); files.AddRange(ProcessAlbumImages(consumer, artist, album, albumFolder, consumerFiles)); @@ -100,7 +100,7 @@ namespace NzbDrone.Core.Extras.Metadata return files; } - public override IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile) + public override IEnumerable CreateAfterTrackImport(Author artist, BookFile trackFile) { var files = new List(); @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Extras.Metadata return files; } - public override IEnumerable CreateAfterTrackImport(Artist artist, Album album, string artistFolder, string albumFolder) + public override IEnumerable CreateAfterTrackImport(Author artist, Book album, string artistFolder, string albumFolder) { var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id); @@ -141,7 +141,7 @@ namespace NzbDrone.Core.Extras.Metadata return files; } - public override IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles) + public override IEnumerable MoveFilesAfterRename(Author artist, List trackFiles) { var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id); var movedFiles = new List(); @@ -154,7 +154,7 @@ namespace NzbDrone.Core.Extras.Metadata foreach (var filePath in distinctTrackFilePaths) { var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles) - .Where(m => m.AlbumId == filePath.AlbumId) + .Where(m => m.BookId == filePath.BookId) .Where(m => m.Type == MetadataType.AlbumImage || m.Type == MetadataType.AlbumMetadata) .ToList(); @@ -210,7 +210,7 @@ namespace NzbDrone.Core.Extras.Metadata return movedFiles; } - public override ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly) + public override ExtraFile Import(Author artist, BookFile trackFile, string path, string extension, bool readOnly) { return null; } @@ -220,7 +220,7 @@ namespace NzbDrone.Core.Extras.Metadata return artistMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); } - private MetadataFile ProcessArtistMetadata(IMetadata consumer, Artist artist, List existingMetadataFiles) + private MetadataFile ProcessArtistMetadata(IMetadata consumer, Author artist, List existingMetadataFiles) { var artistMetadata = consumer.ArtistMetadata(artist); @@ -234,7 +234,7 @@ namespace NzbDrone.Core.Extras.Metadata var metadata = GetMetadataFile(artist, existingMetadataFiles, e => e.Type == MetadataType.ArtistMetadata) ?? new MetadataFile { - ArtistId = artist.Id, + AuthorId = artist.Id, Consumer = consumer.GetType().Name, Type = MetadataType.ArtistMetadata }; @@ -265,7 +265,7 @@ namespace NzbDrone.Core.Extras.Metadata return metadata; } - private MetadataFile ProcessAlbumMetadata(IMetadata consumer, Artist artist, Album album, string albumPath, List existingMetadataFiles) + private MetadataFile ProcessAlbumMetadata(IMetadata consumer, Author artist, Book album, string albumPath, List existingMetadataFiles) { var albumMetadata = consumer.AlbumMetadata(artist, album, albumPath); @@ -276,11 +276,11 @@ namespace NzbDrone.Core.Extras.Metadata var hash = albumMetadata.Contents.SHA256Hash(); - var metadata = GetMetadataFile(artist, existingMetadataFiles, e => e.Type == MetadataType.AlbumMetadata && e.AlbumId == album.Id) ?? + var metadata = GetMetadataFile(artist, existingMetadataFiles, e => e.Type == MetadataType.AlbumMetadata && e.BookId == album.Id) ?? new MetadataFile { - ArtistId = artist.Id, - AlbumId = album.Id, + AuthorId = artist.Id, + BookId = album.Id, Consumer = consumer.GetType().Name, Type = MetadataType.AlbumMetadata }; @@ -311,7 +311,7 @@ namespace NzbDrone.Core.Extras.Metadata return metadata; } - private MetadataFile ProcessTrackMetadata(IMetadata consumer, Artist artist, TrackFile trackFile, List existingMetadataFiles) + private MetadataFile ProcessTrackMetadata(IMetadata consumer, Author artist, BookFile trackFile, List existingMetadataFiles) { var trackMetadata = consumer.TrackMetadata(artist, trackFile); @@ -342,8 +342,8 @@ namespace NzbDrone.Core.Extras.Metadata var metadata = existingMetadata ?? new MetadataFile { - ArtistId = artist.Id, - AlbumId = trackFile.AlbumId, + AuthorId = artist.Id, + BookId = trackFile.BookId, TrackFileId = trackFile.Id, Consumer = consumer.GetType().Name, Type = MetadataType.TrackMetadata, @@ -364,7 +364,7 @@ namespace NzbDrone.Core.Extras.Metadata return metadata; } - private List ProcessArtistImages(IMetadata consumer, Artist artist, List existingMetadataFiles) + private List ProcessArtistImages(IMetadata consumer, Author artist, List existingMetadataFiles) { var result = new List(); @@ -384,7 +384,7 @@ namespace NzbDrone.Core.Extras.Metadata c.RelativePath == image.RelativePath) ?? new MetadataFile { - ArtistId = artist.Id, + AuthorId = artist.Id, Consumer = consumer.GetType().Name, Type = MetadataType.ArtistImage, RelativePath = image.RelativePath, @@ -399,7 +399,7 @@ namespace NzbDrone.Core.Extras.Metadata return result; } - private List ProcessAlbumImages(IMetadata consumer, Artist artist, Album album, string albumFolder, List existingMetadataFiles) + private List ProcessAlbumImages(IMetadata consumer, Author artist, Book album, string albumFolder, List existingMetadataFiles) { var result = new List(); @@ -416,12 +416,12 @@ namespace NzbDrone.Core.Extras.Metadata _otherExtraFileRenamer.RenameOtherExtraFile(artist, fullPath); var metadata = GetMetadataFile(artist, existingMetadataFiles, c => c.Type == MetadataType.AlbumImage && - c.AlbumId == album.Id && + c.BookId == album.Id && c.RelativePath == image.RelativePath) ?? new MetadataFile { - ArtistId = artist.Id, - AlbumId = album.Id, + AuthorId = artist.Id, + BookId = album.Id, Consumer = consumer.GetType().Name, Type = MetadataType.AlbumImage, RelativePath = image.RelativePath, @@ -436,7 +436,7 @@ namespace NzbDrone.Core.Extras.Metadata return result; } - private void DownloadImage(Artist artist, ImageFileResult image) + private void DownloadImage(Author artist, ImageFileResult image) { var fullPath = Path.Combine(artist.Path, image.RelativePath); @@ -469,7 +469,7 @@ namespace NzbDrone.Core.Extras.Metadata _mediaFileAttributeService.SetFilePermissions(path); } - private MetadataFile GetMetadataFile(Artist artist, List existingMetadataFiles, Func predicate) + private MetadataFile GetMetadataFile(Author artist, List existingMetadataFiles, Func predicate) { var matchingMetadataFiles = existingMetadataFiles.Where(predicate).ToList(); diff --git a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs index 35d83e570..7c5ea6fc1 100644 --- a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs +++ b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Extras.Others public override int Order => 2; - public override IEnumerable ProcessFiles(Artist artist, List filesOnDisk, List importedFiles) + public override IEnumerable ProcessFiles(Author artist, List filesOnDisk, List importedFiles) { _logger.Debug("Looking for existing extra files in {0}", artist.Path); @@ -62,23 +62,16 @@ namespace NzbDrone.Core.Extras.Others continue; } - if (localTrack.Tracks.Empty()) + if (localTrack.Album == null) { - _logger.Debug("Cannot find related tracks for: {0}", possibleExtraFile); - continue; - } - - if (localTrack.Tracks.DistinctBy(e => e.TrackFileId).Count() > 1) - { - _logger.Debug("Extra file: {0} does not match existing files.", possibleExtraFile); + _logger.Debug("Cannot find related book for: {0}", possibleExtraFile); continue; } var extraFile = new OtherExtraFile { - ArtistId = artist.Id, - AlbumId = localTrack.Album.Id, - TrackFileId = localTrack.Tracks.First().TrackFileId, + AuthorId = artist.Id, + BookId = localTrack.Album.Id, RelativePath = artist.Path.GetRelativePath(possibleExtraFile), Extension = extension }; diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraFileRenamer.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraFileRenamer.cs index b5f321e61..26548cae4 100644 --- a/src/NzbDrone.Core/Extras/Others/OtherExtraFileRenamer.cs +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraFileRenamer.cs @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Extras.Others { public interface IOtherExtraFileRenamer { - void RenameOtherExtraFile(Artist artist, string path); + void RenameOtherExtraFile(Author artist, string path); } public class OtherExtraFileRenamer : IOtherExtraFileRenamer @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Extras.Others _otherExtraFileService = otherExtraFileService; } - public void RenameOtherExtraFile(Artist artist, string path) + public void RenameOtherExtraFile(Author artist, string path) { if (!_diskProvider.FileExists(path)) { @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Extras.Others } } - private void RemoveOtherExtraFile(Artist artist, string path) + private void RemoveOtherExtraFile(Author artist, string path) { if (!_diskProvider.FileExists(path)) { diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs index 10c5a8a73..63e4b3be7 100644 --- a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs @@ -26,22 +26,22 @@ namespace NzbDrone.Core.Extras.Others public override int Order => 2; - public override IEnumerable CreateAfterArtistScan(Artist artist, List trackFiles) + public override IEnumerable CreateAfterArtistScan(Author artist, List trackFiles) { return Enumerable.Empty(); } - public override IEnumerable CreateAfterTrackImport(Artist artist, TrackFile trackFile) + public override IEnumerable CreateAfterTrackImport(Author artist, BookFile trackFile) { return Enumerable.Empty(); } - public override IEnumerable CreateAfterTrackImport(Artist artist, Album album, string artistFolder, string albumFolder) + public override IEnumerable CreateAfterTrackImport(Author artist, Book album, string artistFolder, string albumFolder) { return Enumerable.Empty(); } - public override IEnumerable MoveFilesAfterRename(Artist artist, List trackFiles) + public override IEnumerable MoveFilesAfterRename(Author artist, List trackFiles) { var extraFiles = _otherExtraFileService.GetFilesByArtist(artist.Id); var movedFiles = new List(); @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Extras.Others return movedFiles; } - public override ExtraFile Import(Artist artist, TrackFile trackFile, string path, string extension, bool readOnly) + public override ExtraFile Import(Author artist, BookFile trackFile, string path, string extension, bool readOnly) { var extraFile = ImportFile(artist, trackFile, path, readOnly, extension, null); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs index 70d3dd68a..cfa07506d 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs @@ -24,47 +24,14 @@ namespace NzbDrone.Core.HealthCheck.Checks var monoVersion = _platformInfo.Version; - // Known buggy Mono versions - if (monoVersion == new Version("4.4.0") || monoVersion == new Version("4.4.1")) - { - _logger.Debug("Mono version {0}", monoVersion); - return new HealthCheck(GetType(), - HealthCheckResult.Error, - $"Currently installed Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version", - "#currently-installed-mono-version-is-old-and-unsupported"); - } - // Currently best stable Mono version (5.18 gets us .net 4.7.2 support) var bestVersion = new Version("5.20"); - var targetVersion = new Version("5.18"); - if (monoVersion >= targetVersion) + if (monoVersion >= bestVersion) { - _logger.Debug("Mono version is {0} or better: {1}", targetVersion, monoVersion); + _logger.Debug("Mono version is {0} or better: {1}", bestVersion, monoVersion); return new HealthCheck(GetType()); } - // Stable Mono versions - var stableVersion = new Version("5.16"); - if (monoVersion >= stableVersion) - { - _logger.Debug("Mono version is {0} or better: {1}", stableVersion, monoVersion); - return new HealthCheck(GetType(), - HealthCheckResult.Notice, - $"Currently installed Mono version {monoVersion} is supported but upgrading to {bestVersion} is recommended.", - "#currently-installed-mono-version-is-supported-but-upgrading-is-recommended"); - } - - // Old but supported Mono versions, there are known bugs - var supportedVersion = new Version("5.4"); - if (monoVersion >= supportedVersion) - { - _logger.Debug("Mono version is {0} or better: {1}", supportedVersion, monoVersion); - return new HealthCheck(GetType(), - HealthCheckResult.Warning, - $"Currently installed Mono version {monoVersion} is supported but has some known issues. Please upgrade Mono to version {bestVersion}.", - "#currently-installed-mono-version-is-supported-but-upgrading-is-recommended"); - } - return new HealthCheck(GetType(), HealthCheckResult.Error, $"Currently installed Mono version {monoVersion} is old and unsupported. Please upgrade Mono to version {bestVersion}.", diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 8d9592361..18aa78a60 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -15,15 +15,13 @@ namespace NzbDrone.Core.History Data = new Dictionary(); } - public int TrackId { get; set; } - public int AlbumId { get; set; } - public int ArtistId { get; set; } + public int BookId { get; set; } + public int AuthorId { get; set; } public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } - public Album Album { get; set; } - public Artist Artist { get; set; } - public Track Track { get; set; } + public Book Album { get; set; } + public Author Artist { get; set; } public HistoryEventType EventType { get; set; } public Dictionary Data { get; set; } diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index ba615b1a2..d674bc251 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Dapper; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; @@ -11,13 +10,13 @@ namespace NzbDrone.Core.History { public interface IHistoryRepository : IBasicRepository { - History MostRecentForAlbum(int albumId); + History MostRecentForAlbum(int bookId); History MostRecentForDownloadId(string downloadId); List FindByDownloadId(string downloadId); - List GetByArtist(int artistId, HistoryEventType? eventType); - List GetByAlbum(int albumId, HistoryEventType? eventType); - List FindDownloadHistory(int idArtistId, QualityModel quality); - void DeleteForArtist(int artistId); + List GetByArtist(int authorId, HistoryEventType? eventType); + List GetByAlbum(int bookId, HistoryEventType? eventType); + List FindDownloadHistory(int idAuthorId, QualityModel quality); + void DeleteForArtist(int authorId); List Since(DateTime date, HistoryEventType? eventType); } @@ -28,9 +27,9 @@ namespace NzbDrone.Core.History { } - public History MostRecentForAlbum(int albumId) + public History MostRecentForAlbum(int bookId) { - return Query(h => h.AlbumId == albumId) + return Query(h => h.BookId == bookId) .OrderByDescending(h => h.Date) .FirstOrDefault(); } @@ -44,10 +43,10 @@ namespace NzbDrone.Core.History public List FindByDownloadId(string downloadId) { - return _database.QueryJoined( + return _database.QueryJoined( Builder() - .Join((h, a) => h.ArtistId == a.Id) - .Join((h, a) => h.AlbumId == a.Id) + .Join((h, a) => h.AuthorId == a.Id) + .Join((h, a) => h.BookId == a.Id) .Where(h => h.DownloadId == downloadId), (history, artist, album) => { @@ -57,9 +56,9 @@ namespace NzbDrone.Core.History }).ToList(); } - public List GetByArtist(int artistId, HistoryEventType? eventType) + public List GetByArtist(int authorId, HistoryEventType? eventType) { - var builder = Builder().Where(h => h.ArtistId == artistId); + var builder = Builder().Where(h => h.AuthorId == authorId); if (eventType.HasValue) { @@ -69,18 +68,18 @@ namespace NzbDrone.Core.History return Query(builder).OrderByDescending(h => h.Date).ToList(); } - public List GetByAlbum(int albumId, HistoryEventType? eventType) + public List GetByAlbum(int bookId, HistoryEventType? eventType) { var builder = Builder() - .Join((h, a) => h.AlbumId == a.Id) - .Where(h => h.AlbumId == albumId); + .Join((h, a) => h.BookId == a.Id) + .Where(h => h.BookId == bookId); if (eventType.HasValue) { builder.Where(h => h.EventType == eventType); } - return _database.QueryJoined( + return _database.QueryJoined( builder, (history, album) => { @@ -89,30 +88,29 @@ namespace NzbDrone.Core.History }).OrderByDescending(h => h.Date).ToList(); } - public List FindDownloadHistory(int idArtistId, QualityModel quality) + public List FindDownloadHistory(int idAuthorId, QualityModel quality) { var allowed = new[] { HistoryEventType.Grabbed, HistoryEventType.DownloadFailed, HistoryEventType.TrackFileImported }; - return Query(h => h.ArtistId == idArtistId && + return Query(h => h.AuthorId == idAuthorId && h.Quality == quality && allowed.Contains(h.EventType)); } - public void DeleteForArtist(int artistId) + public void DeleteForArtist(int authorId) { - Delete(c => c.ArtistId == artistId); + Delete(c => c.AuthorId == authorId); } protected override SqlBuilder PagedBuilder() => new SqlBuilder() - .Join((h, a) => h.ArtistId == a.Id) - .Join((h, a) => h.AlbumId == a.Id) - .LeftJoin((h, t) => h.TrackId == t.Id); + .Join((h, a) => h.AuthorId == a.Id) + .Join((h, a) => h.BookId == a.Id); + protected override IEnumerable PagedQuery(SqlBuilder builder) => - _database.QueryJoined(builder, (history, artist, album, track) => + _database.QueryJoined(builder, (history, artist, album) => { history.Artist = artist; history.Album = album; - history.Track = track; return history; }); diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index d4e7d8dc1..6dcea9e92 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -19,11 +19,11 @@ namespace NzbDrone.Core.History public interface IHistoryService { PagingSpec Paged(PagingSpec pagingSpec); - History MostRecentForAlbum(int albumId); + History MostRecentForAlbum(int bookId); History MostRecentForDownloadId(string downloadId); History Get(int historyId); - List GetByArtist(int artistId, HistoryEventType? eventType); - List GetByAlbum(int albumId, HistoryEventType? eventType); + List GetByArtist(int authorId, HistoryEventType? eventType); + List GetByAlbum(int bookId, HistoryEventType? eventType); List Find(string downloadId, HistoryEventType eventType); List FindByDownloadId(string downloadId); List Since(DateTime date, HistoryEventType? eventType); @@ -56,9 +56,9 @@ namespace NzbDrone.Core.History return _historyRepository.GetPaged(pagingSpec); } - public History MostRecentForAlbum(int albumId) + public History MostRecentForAlbum(int bookId) { - return _historyRepository.MostRecentForAlbum(albumId); + return _historyRepository.MostRecentForAlbum(bookId); } public History MostRecentForDownloadId(string downloadId) @@ -71,14 +71,14 @@ namespace NzbDrone.Core.History return _historyRepository.Get(historyId); } - public List GetByArtist(int artistId, HistoryEventType? eventType) + public List GetByArtist(int authorId, HistoryEventType? eventType) { - return _historyRepository.GetByArtist(artistId, eventType); + return _historyRepository.GetByArtist(authorId, eventType); } - public List GetByAlbum(int albumId, HistoryEventType? eventType) + public List GetByAlbum(int bookId, HistoryEventType? eventType) { - return _historyRepository.GetByAlbum(albumId, eventType); + return _historyRepository.GetByAlbum(bookId, eventType); } public List Find(string downloadId, HistoryEventType eventType) @@ -95,12 +95,12 @@ namespace NzbDrone.Core.History { _logger.Debug("Trying to find downloadId for {0} from history", trackedDownload.ImportedTrack.Path); - var albumIds = trackedDownload.TrackInfo.Tracks.Select(c => c.AlbumId).ToList(); + var bookIds = new List { trackedDownload.TrackInfo.Album.Id }; var allHistory = _historyRepository.FindDownloadHistory(trackedDownload.TrackInfo.Artist.Id, trackedDownload.ImportedTrack.Quality); //Find download related items for these episdoes - var albumsHistory = allHistory.Where(h => albumIds.Contains(h.AlbumId)).ToList(); + var albumsHistory = allHistory.Where(h => bookIds.Contains(h.BookId)).ToList(); var processedDownloadId = albumsHistory .Where(c => c.EventType != HistoryEventType.Grabbed && c.DownloadId != null) @@ -112,23 +112,22 @@ namespace NzbDrone.Core.History if (stillDownloading.Any()) { - foreach (var matchingHistory in trackedDownload.TrackInfo.Tracks.Select(e => stillDownloading.Where(c => c.AlbumId == e.AlbumId).ToList())) + var matchingHistory = stillDownloading.Where(c => c.BookId == trackedDownload.TrackInfo.Album.Id).ToList(); + + if (matchingHistory.Count != 1) + { + return null; + } + + var newDownloadId = matchingHistory.Single().DownloadId; + + if (downloadId == null || downloadId == newDownloadId) { - if (matchingHistory.Count != 1) - { - return null; - } - - var newDownloadId = matchingHistory.Single().DownloadId; - - if (downloadId == null || downloadId == newDownloadId) - { - downloadId = newDownloadId; - } - else - { - return null; - } + downloadId = newDownloadId; + } + else + { + return null; } } @@ -145,8 +144,8 @@ namespace NzbDrone.Core.History Date = DateTime.UtcNow, Quality = message.Album.ParsedAlbumInfo.Quality, SourceTitle = message.Album.Release.Title, - ArtistId = album.ArtistId, - AlbumId = album.Id, + AuthorId = album.AuthorId, + BookId = album.Id, DownloadId = message.DownloadId }; @@ -190,8 +189,8 @@ namespace NzbDrone.Core.History Date = DateTime.UtcNow, Quality = message.TrackedDownload.RemoteAlbum.ParsedAlbumInfo?.Quality ?? new QualityModel(), SourceTitle = message.TrackedDownload.DownloadItem.Title, - ArtistId = album.ArtistId, - AlbumId = album.Id, + AuthorId = album.AuthorId, + BookId = album.Id, DownloadId = message.TrackedDownload.DownloadItem.DownloadId }; @@ -214,33 +213,29 @@ namespace NzbDrone.Core.History downloadId = FindDownloadId(message); } - foreach (var track in message.TrackInfo.Tracks) + var history = new History { - var history = new History - { - EventType = HistoryEventType.TrackFileImported, - Date = DateTime.UtcNow, - Quality = message.TrackInfo.Quality, - SourceTitle = message.ImportedTrack.SceneName ?? Path.GetFileNameWithoutExtension(message.TrackInfo.Path), - ArtistId = message.TrackInfo.Artist.Id, - AlbumId = message.TrackInfo.Album.Id, - TrackId = track.Id, - DownloadId = downloadId - }; - - //Won't have a value since we publish this event before saving to DB. - //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); - history.Data.Add("DroppedPath", message.TrackInfo.Path); - history.Data.Add("ImportedPath", message.ImportedTrack.Path); - history.Data.Add("DownloadClient", message.DownloadClient); - - _historyRepository.Insert(history); - } + EventType = HistoryEventType.TrackFileImported, + Date = DateTime.UtcNow, + Quality = message.TrackInfo.Quality, + SourceTitle = message.ImportedTrack.SceneName ?? Path.GetFileNameWithoutExtension(message.TrackInfo.Path), + AuthorId = message.TrackInfo.Artist.Id, + BookId = message.TrackInfo.Album.Id, + DownloadId = downloadId + }; + + //Won't have a value since we publish this event before saving to DB. + //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); + history.Data.Add("DroppedPath", message.TrackInfo.Path); + history.Data.Add("ImportedPath", message.ImportedTrack.Path); + history.Data.Add("DownloadClient", message.DownloadClient); + + _historyRepository.Insert(history); } public void Handle(DownloadFailedEvent message) { - foreach (var albumId in message.AlbumIds) + foreach (var bookId in message.BookIds) { var history = new History { @@ -248,8 +243,8 @@ namespace NzbDrone.Core.History Date = DateTime.UtcNow, Quality = message.Quality, SourceTitle = message.SourceTitle, - ArtistId = message.ArtistId, - AlbumId = albumId, + AuthorId = message.AuthorId, + BookId = bookId, DownloadId = message.DownloadId }; @@ -270,8 +265,8 @@ namespace NzbDrone.Core.History Date = DateTime.UtcNow, Quality = message.TrackedDownload.RemoteAlbum.ParsedAlbumInfo?.Quality ?? new QualityModel(), SourceTitle = message.TrackedDownload.DownloadItem.Title, - ArtistId = album.ArtistId, - AlbumId = album.Id, + AuthorId = album.AuthorId, + BookId = album.Id, DownloadId = message.TrackedDownload.DownloadItem.DownloadId }; @@ -292,23 +287,19 @@ namespace NzbDrone.Core.History return; } - foreach (var track in message.TrackFile.Tracks.Value) + var history = new History { - var history = new History - { - EventType = HistoryEventType.TrackFileDeleted, - Date = DateTime.UtcNow, - Quality = message.TrackFile.Quality, - SourceTitle = message.TrackFile.Path, - ArtistId = message.TrackFile.Artist.Value.Id, - AlbumId = message.TrackFile.AlbumId, - TrackId = track.Id, - }; + EventType = HistoryEventType.TrackFileDeleted, + Date = DateTime.UtcNow, + Quality = message.TrackFile.Quality, + SourceTitle = message.TrackFile.Path, + AuthorId = message.TrackFile.Artist.Value.Id, + BookId = message.TrackFile.BookId + }; - history.Data.Add("Reason", message.Reason.ToString()); + history.Data.Add("Reason", message.Reason.ToString()); - _historyRepository.Insert(history); - } + _historyRepository.Insert(history); } public void Handle(TrackFileRenamedEvent message) @@ -316,53 +307,45 @@ namespace NzbDrone.Core.History var sourcePath = message.OriginalPath; var path = message.TrackFile.Path; - foreach (var track in message.TrackFile.Tracks.Value) + var history = new History { - var history = new History - { - EventType = HistoryEventType.TrackFileRenamed, - Date = DateTime.UtcNow, - Quality = message.TrackFile.Quality, - SourceTitle = message.OriginalPath, - ArtistId = message.TrackFile.Artist.Value.Id, - AlbumId = message.TrackFile.AlbumId, - TrackId = track.Id, - }; - - history.Data.Add("SourcePath", sourcePath); - history.Data.Add("Path", path); - - _historyRepository.Insert(history); - } + EventType = HistoryEventType.TrackFileRenamed, + Date = DateTime.UtcNow, + Quality = message.TrackFile.Quality, + SourceTitle = message.OriginalPath, + AuthorId = message.TrackFile.Artist.Value.Id, + BookId = message.TrackFile.BookId + }; + + history.Data.Add("SourcePath", sourcePath); + history.Data.Add("Path", path); + + _historyRepository.Insert(history); } public void Handle(TrackFileRetaggedEvent message) { var path = message.TrackFile.Path; - foreach (var track in message.TrackFile.Tracks.Value) + var history = new History { - var history = new History - { - EventType = HistoryEventType.TrackFileRetagged, - Date = DateTime.UtcNow, - Quality = message.TrackFile.Quality, - SourceTitle = path, - ArtistId = message.TrackFile.Artist.Value.Id, - AlbumId = message.TrackFile.AlbumId, - TrackId = track.Id, - }; - - history.Data.Add("TagsScrubbed", message.Scrubbed.ToString()); - history.Data.Add("Diff", message.Diff.Select(x => new - { - Field = x.Key, - OldValue = x.Value.Item1, - NewValue = x.Value.Item2 - }).ToJson()); + EventType = HistoryEventType.TrackFileRetagged, + Date = DateTime.UtcNow, + Quality = message.TrackFile.Quality, + SourceTitle = path, + AuthorId = message.TrackFile.Artist.Value.Id, + BookId = message.TrackFile.BookId + }; + + history.Data.Add("TagsScrubbed", message.Scrubbed.ToString()); + history.Data.Add("Diff", message.Diff.Select(x => new + { + Field = x.Key, + OldValue = x.Value.Item1, + NewValue = x.Value.Item2 + }).ToJson()); - _historyRepository.Insert(history); - } + _historyRepository.Insert(history); } public void Handle(ArtistDeletedEvent message) @@ -373,7 +356,7 @@ namespace NzbDrone.Core.History public void Handle(DownloadIgnoredEvent message) { var historyToAdd = new List(); - foreach (var albumId in message.AlbumIds) + foreach (var bookId in message.BookIds) { var history = new History { @@ -381,8 +364,8 @@ namespace NzbDrone.Core.History Date = DateTime.UtcNow, Quality = message.Quality, SourceTitle = message.SourceTitle, - ArtistId = message.ArtistId, - AlbumId = albumId, + AuthorId = message.AuthorId, + BookId = bookId, DownloadId = message.DownloadId }; diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs index 3b06fdba4..f69d856ba 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs @@ -28,8 +28,8 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type = 1 - GROUP BY ArtistId, Consumer - HAVING COUNT(ArtistId) > 1 + GROUP BY AuthorId, Consumer + HAVING COUNT(AuthorId) > 1 )"); } } @@ -42,8 +42,8 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type = 6 - GROUP BY AlbumId, Consumer - HAVING COUNT(AlbumId) > 1 + GROUP BY BookId, Consumer + HAVING COUNT(BookId) > 1 )"); } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs index f80b93f07..d67595fa6 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs @@ -3,11 +3,11 @@ using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { - public class CleanupOrphanedAlbums : IHousekeepingTask + public class CleanupOrphanedBooks : IHousekeepingTask { private readonly IMainDatabase _database; - public CleanupOrphanedAlbums(IMainDatabase database) + public CleanupOrphanedBooks(IMainDatabase database) { _database = database; } @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { using (var mapper = _database.OpenConnection()) { - mapper.Execute(@"DELETE FROM Albums + mapper.Execute(@"DELETE FROM Books WHERE Id IN ( - SELECT Albums.Id FROM Albums - LEFT OUTER JOIN Artists - ON Albums.ArtistMetadataId = Artists.ArtistMetadataId - WHERE Artists.Id IS NULL)"); + SELECT Books.Id FROM Books + LEFT OUTER JOIN Authors + ON Books.AuthorMetadataId = Authors.AuthorMetadataId + WHERE Authors.Id IS NULL)"); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs index 52f674312..27d8f1188 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs @@ -3,11 +3,11 @@ using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { - public class CleanupOrphanedArtistMetadata : IHousekeepingTask + public class CleanupOrphanedAuthorMetadata : IHousekeepingTask { private readonly IMainDatabase _database; - public CleanupOrphanedArtistMetadata(IMainDatabase database) + public CleanupOrphanedAuthorMetadata(IMainDatabase database) { _database = database; } @@ -16,13 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { using (var mapper = _database.OpenConnection()) { - mapper.Execute(@"DELETE FROM ArtistMetadata + mapper.Execute(@"DELETE FROM AuthorMetadata WHERE Id IN ( - SELECT ArtistMetadata.Id FROM ArtistMetadata - LEFT OUTER JOIN Albums ON Albums.ArtistMetadataId = ArtistMetadata.Id - LEFT OUTER JOIN Tracks ON Tracks.ArtistMetadataId = ArtistMetadata.Id - LEFT OUTER JOIN Artists ON Artists.ArtistMetadataId = ArtistMetadata.Id - WHERE Albums.Id IS NULL AND Tracks.Id IS NULL AND Artists.Id IS NULL)"); + SELECT AuthorMetadata.Id FROM AuthorMetadata + LEFT OUTER JOIN Books ON Books.AuthorMetadataId = AuthorMetadata.Id + LEFT OUTER JOIN Authors ON Authors.AuthorMetadataId = AuthorMetadata.Id + WHERE Books.Id IS NULL AND Authors.Id IS NULL)"); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs index fa0a5f020..a0044f1d0 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs @@ -19,9 +19,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers mapper.Execute(@"DELETE FROM Blacklist WHERE Id IN ( SELECT Blacklist.Id FROM Blacklist - LEFT OUTER JOIN Artists - ON Blacklist.ArtistId = Artists.Id - WHERE Artists.Id IS NULL)"); + LEFT OUTER JOIN Authors + ON Blacklist.AuthorId = Authors.Id + WHERE Authors.Id IS NULL)"); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs index 43b298605..2937df4da 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs @@ -25,9 +25,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers mapper.Execute(@"DELETE FROM History WHERE Id IN ( SELECT History.Id FROM History - LEFT OUTER JOIN Artists - ON History.ArtistId = Artists.Id - WHERE Artists.Id IS NULL)"); + LEFT OUTER JOIN Authors + ON History.AuthorId = Authors.Id + WHERE Authors.Id IS NULL)"); } } @@ -38,9 +38,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers mapper.Execute(@"DELETE FROM History WHERE Id IN ( SELECT History.Id FROM History - LEFT OUTER JOIN Albums - ON History.AlbumId = Albums.Id - WHERE Albums.Id IS NULL)"); + LEFT OUTER JOIN Books + ON History.BookId = Books.Id + WHERE Books.Id IS NULL)"); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs index 2afd5feea..750aa7dcb 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers DeleteOrphanedByArtist(); DeleteOrphanedByAlbum(); DeleteOrphanedByTrackFile(); - DeleteWhereAlbumIdIsZero(); + DeleteWhereBookIdIsZero(); DeleteWhereTrackFileIsZero(); } @@ -28,9 +28,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT MetadataFiles.Id FROM MetadataFiles - LEFT OUTER JOIN Artists - ON MetadataFiles.ArtistId = Artists.Id - WHERE Artists.Id IS NULL)"); + LEFT OUTER JOIN Authors + ON MetadataFiles.AuthorId = Authors.Id + WHERE Authors.Id IS NULL)"); } } @@ -41,10 +41,10 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT MetadataFiles.Id FROM MetadataFiles - LEFT OUTER JOIN Albums - ON MetadataFiles.AlbumId = Albums.Id - WHERE MetadataFiles.AlbumId > 0 - AND Albums.Id IS NULL)"); + LEFT OUTER JOIN Books + ON MetadataFiles.BookId = Books.Id + WHERE MetadataFiles.BookId > 0 + AND Books.Id IS NULL)"); } } @@ -55,14 +55,14 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT MetadataFiles.Id FROM MetadataFiles - LEFT OUTER JOIN TrackFiles - ON MetadataFiles.TrackFileId = TrackFiles.Id + LEFT OUTER JOIN BookFiles + ON MetadataFiles.TrackFileId = BookFiles.Id WHERE MetadataFiles.TrackFileId > 0 - AND TrackFiles.Id IS NULL)"); + AND BookFiles.Id IS NULL)"); } } - private void DeleteWhereAlbumIdIsZero() + private void DeleteWhereBookIdIsZero() { using (var mapper = _database.OpenConnection()) { @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type IN (4, 6) - AND AlbumId = 0)"); + AND BookId = 0)"); } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs index d1ce0fc3b..e613481a4 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs @@ -19,9 +19,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers mapper.Execute(@"DELETE FROM PendingReleases WHERE Id IN ( SELECT PendingReleases.Id FROM PendingReleases - LEFT OUTER JOIN Artists - ON PendingReleases.ArtistId = Artists.Id - WHERE Artists.Id IS NULL)"); + LEFT OUTER JOIN Authors + ON PendingReleases.AuthorId = Authors.Id + WHERE Authors.Id IS NULL)"); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs deleted file mode 100644 index e2613f220..000000000 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Dapper; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Housekeeping.Housekeepers -{ - public class CleanupOrphanedReleases : IHousekeepingTask - { - private readonly IMainDatabase _database; - - public CleanupOrphanedReleases(IMainDatabase database) - { - _database = database; - } - - public void Clean() - { - using (var mapper = _database.OpenConnection()) - { - mapper.Execute(@"DELETE FROM AlbumReleases - WHERE Id IN ( - SELECT AlbumReleases.Id FROM AlbumReleases - LEFT OUTER JOIN Albums - ON AlbumReleases.AlbumId = Albums.Id - WHERE Albums.Id IS NULL)"); - } - } - } -} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs index 57657de83..37a13499b 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs @@ -3,11 +3,11 @@ using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { - public class CleanupOrphanedTrackFiles : IHousekeepingTask + public class CleanupOrphanedBookFiles : IHousekeepingTask { private readonly IMainDatabase _database; - public CleanupOrphanedTrackFiles(IMainDatabase database) + public CleanupOrphanedBookFiles(IMainDatabase database) { _database = database; } @@ -17,22 +17,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers using (var mapper = _database.OpenConnection()) { // Unlink where track no longer exists - mapper.Execute(@"UPDATE TrackFiles - SET AlbumId = 0 + mapper.Execute(@"UPDATE BookFiles + SET BookId = 0 WHERE Id IN ( - SELECT TrackFiles.Id FROM TrackFiles - LEFT OUTER JOIN Tracks - ON TrackFiles.Id = Tracks.TrackFileId - WHERE Tracks.Id IS NULL)"); - - // Unlink Tracks where the Trackfiles entry no longer exists - mapper.Execute(@"UPDATE Tracks - SET TrackFileId = 0 - WHERE Id IN ( - SELECT Tracks.Id FROM Tracks - LEFT OUTER JOIN TrackFiles - ON Tracks.TrackFileId = TrackFiles.Id - WHERE TrackFiles.Id IS NULL)"); + SELECT BookFiles.Id FROM BookFiles + LEFT OUTER JOIN Books + ON BookFiles.BookId = Books.Id + WHERE Books.Id IS NULL)"); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs deleted file mode 100644 index ff2017c5c..000000000 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Dapper; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Housekeeping.Housekeepers -{ - public class CleanupOrphanedTracks : IHousekeepingTask - { - private readonly IMainDatabase _database; - - public CleanupOrphanedTracks(IMainDatabase database) - { - _database = database; - } - - public void Clean() - { - using (var mapper = _database.OpenConnection()) - { - mapper.Execute(@"DELETE FROM Tracks - WHERE Id IN ( - SELECT Tracks.Id FROM Tracks - LEFT OUTER JOIN AlbumReleases - ON Tracks.AlbumReleaseId = AlbumReleases.Id - WHERE AlbumReleases.Id IS NULL)"); - } - } - } -} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs index 9b993a26d..79c42e722 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { using (var mapper = _database.OpenConnection()) { - var usedTags = new[] { "Artists", "Notifications", "DelayProfiles", "ReleaseProfiles" } + var usedTags = new[] { "Authors", "Notifications", "DelayProfiles", "ReleaseProfiles" } .SelectMany(v => GetUsedTags(v, mapper)) .Distinct() .ToArray(); diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs index f4b3de7e6..3bdf2544f 100644 --- a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs @@ -79,7 +79,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions return; } - var existingExclusion = _repo.FindByForeignId(message.Artist.ForeignArtistId); + var existingExclusion = _repo.FindByForeignId(message.Artist.ForeignAuthorId); if (existingExclusion != null) { @@ -88,7 +88,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions var importExclusion = new ImportListExclusion { - ForeignId = message.Artist.ForeignArtistId, + ForeignId = message.Artist.ForeignAuthorId, Name = message.Artist.Name }; @@ -102,7 +102,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions return; } - var existingExclusion = _repo.FindByForeignId(message.Album.ForeignAlbumId); + var existingExclusion = _repo.FindByForeignId(message.Album.ForeignBookId); if (existingExclusion != null) { @@ -111,8 +111,8 @@ namespace NzbDrone.Core.ImportLists.Exclusions var importExclusion = new ImportListExclusion { - ForeignId = message.Album.ForeignAlbumId, - Name = $"{message.Album.ArtistMetadata.Value.Name} - {message.Album.Title}" + ForeignId = message.Album.ForeignBookId, + Name = $"{message.Album.AuthorMetadata.Value.Name} - {message.Album.Title}" }; _repo.Insert(importExclusion); diff --git a/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsBookshelf.cs b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsBookshelf.cs new file mode 100644 index 000000000..576f1b8b8 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsBookshelf.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MetadataSource.Goodreads; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists.Goodreads +{ + public class GoodreadsBookshelf : GoodreadsImportListBase + { + public GoodreadsBookshelf(IImportListStatusService importListStatusService, + IConfigService configService, + IParsingService parsingService, + IHttpClient httpClient, + Logger logger) + : base(importListStatusService, configService, parsingService, httpClient, logger) + { + } + + public override string Name => "Goodreads Bookshelves"; + + public override IList Fetch() + { + return CleanupListItems(Settings.PlaylistIds.SelectMany(x => Fetch(x)).ToList()); + } + + public IList Fetch(string shelf) + { + var reviews = new List(); + var page = 0; + + while (true) + { + var curr = GetReviews(shelf, ++page); + + if (curr == null || curr.Count == 0) + { + break; + } + + reviews.AddRange(curr); + } + + return reviews.Select(x => new ImportListItemInfo + { + Artist = x.Book.Authors.First().Name.CleanSpaces(), + Album = x.Book.TitleWithoutSeries.CleanSpaces(), + AlbumMusicBrainzId = x.Book.Uri.Replace("kca://book/", string.Empty) + }).ToList(); + } + + public override object RequestAction(string action, IDictionary query) + { + if (action == "getPlaylists") + { + if (Settings.AccessToken.IsNullOrWhiteSpace()) + { + return new + { + playlists = new List() + }; + } + + Settings.Validate().Filter("AccessToken").ThrowOnError(); + + var shelves = new List(); + var page = 0; + + while (true) + { + var curr = GetShelfList(++page); + if (curr == null || curr.Count == 0) + { + break; + } + + shelves.AddRange(curr); + } + + return new + { + options = new + { + user = Settings.UserName, + playlists = shelves.OrderBy(p => p.Name) + .Select(p => new + { + id = p.Name, + name = p.Name + }) + } + }; + } + else + { + return base.RequestAction(action, query); + } + } + + private IReadOnlyList GetShelfList(int page) + { + try + { + var builder = RequestBuilder() + .SetSegment("route", $"shelf/list.xml") + .AddQueryParam("user_id", Settings.UserId) + .AddQueryParam("page", page); + + var httpResponse = OAuthGet(builder); + + return httpResponse.Deserialize>("shelves").List; + } + catch (Exception ex) + { + _logger.Warn(ex, "Error fetching bookshelves from Goodreads"); + return new List(); + } + } + + private IReadOnlyList GetReviews(string shelf, int page) + { + try + { + var builder = RequestBuilder() + .SetSegment("route", $"review/list.xml") + .AddQueryParam("v", 2) + .AddQueryParam("id", Settings.UserId) + .AddQueryParam("shelf", shelf) + .AddQueryParam("per_page", 200) + .AddQueryParam("page", page); + + var httpResponse = OAuthGet(builder); + + return httpResponse.Deserialize>("reviews").List; + } + catch (Exception ex) + { + _logger.Warn(ex, "Error fetching bookshelves from Goodreads"); + return new List(); + } + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsBookshelfSettings.cs b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsBookshelfSettings.cs new file mode 100644 index 000000000..8f74e5ba7 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsBookshelfSettings.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.ImportLists.Goodreads +{ + public class GoodreadsBookshelfSettingsValidator : GoodreadsSettingsBaseValidator + { + public GoodreadsBookshelfSettingsValidator() + : base() + { + RuleFor(c => c.PlaylistIds).NotEmpty(); + } + } + + public class GoodreadsBookshelfSettings : GoodreadsSettingsBase + { + public GoodreadsBookshelfSettings() + { + PlaylistIds = new string[] { }; + } + + [FieldDefinition(1, Label = "Bookshelves", Type = FieldType.Playlist)] + public IEnumerable PlaylistIds { get; set; } + + protected override AbstractValidator Validator => new GoodreadsBookshelfSettingsValidator(); + } +} diff --git a/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsException.cs b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsException.cs new file mode 100644 index 000000000..cb0b09f3c --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsException.cs @@ -0,0 +1,31 @@ +using System; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.ImportLists.Goodreads +{ + public class GoodreadsException : NzbDroneException + { + public GoodreadsException(string message) + : base(message) + { + } + + public GoodreadsException(string message, params object[] args) + : base(message, args) + { + } + + public GoodreadsException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + public class GoodreadsAuthorizationException : GoodreadsException + { + public GoodreadsAuthorizationException(string message) + : base(message) + { + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsImportListBase.cs b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsImportListBase.cs new file mode 100644 index 000000000..39c03be0d --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsImportListBase.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Web; +using System.Xml.Linq; +using System.Xml.XPath; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.OAuth; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MetadataSource.Goodreads; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.ImportLists.Goodreads +{ + public abstract class GoodreadsImportListBase : ImportListBase + where TSettings : GoodreadsSettingsBase, new() + { + protected readonly IHttpClient _httpClient; + + protected GoodreadsImportListBase(IImportListStatusService importListStatusService, + IConfigService configService, + IParsingService parsingService, + IHttpClient httpClient, + Logger logger) + : base(importListStatusService, configService, parsingService, logger) + { + _httpClient = httpClient; + } + + public override ImportListType ListType => ImportListType.Goodreads; + + public string AccessToken => Settings.AccessToken; + + protected HttpRequestBuilder RequestBuilder() => new HttpRequestBuilder("https://www.goodreads.com/{route}") + .AddQueryParam("key", "xQh8LhdTztb9u3cL26RqVg", true) + .AddQueryParam("_nc", "1") + .KeepAlive(); + + protected override void Test(List failures) + { + failures.AddIfNotNull(TestConnection()); + } + + private ValidationFailure TestConnection() + { + try + { + GetUser(); + return null; + } + catch (Common.Http.HttpException ex) + { + _logger.Warn(ex, "Goodreads Authentication Error"); + return new ValidationFailure(string.Empty, $"Goodreads authentication error: {ex.Message}"); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to connect to Goodreads"); + + return new ValidationFailure(string.Empty, "Unable to connect to import list, check the log for more details"); + } + } + + public override object RequestAction(string action, IDictionary query) + { + if (action == "startOAuth") + { + if (query["callbackUrl"].IsNullOrWhiteSpace()) + { + throw new BadRequestException("QueryParam callbackUrl invalid."); + } + + var oAuthRequest = OAuthRequest.ForRequestToken(Settings.ConsumerKey, Settings.ConsumerSecret, query["callbackUrl"]); + oAuthRequest.RequestUrl = Settings.OAuthRequestTokenUrl; + var qscoll = OAuthQuery(oAuthRequest); + + var url = string.Format("{0}?oauth_token={1}&oauth_callback={2}", Settings.OAuthUrl, qscoll["oauth_token"], query["callbackUrl"]); + + return new + { + OauthUrl = url, + RequestTokenSecret = qscoll["oauth_token_secret"] + }; + } + else if (action == "getOAuthToken") + { + if (query["oauth_token"].IsNullOrWhiteSpace()) + { + throw new BadRequestException("QueryParam oauth_token invalid."); + } + + if (query["requestTokenSecret"].IsNullOrWhiteSpace()) + { + throw new BadRequestException("Missing requestTokenSecret."); + } + + var oAuthRequest = OAuthRequest.ForAccessToken(Settings.ConsumerKey, Settings.ConsumerSecret, query["oauth_token"], query["requestTokenSecret"], ""); + oAuthRequest.RequestUrl = Settings.OAuthAccessTokenUrl; + var qscoll = OAuthQuery(oAuthRequest); + + Settings.AccessToken = qscoll["oauth_token"]; + Settings.AccessTokenSecret = qscoll["oauth_token_secret"]; + + var user = GetUser(); + + return new + { + Settings.AccessToken, + Settings.AccessTokenSecret, + RequestTokenSecret = "", + UserId = user.Item1, + UserName = user.Item2 + }; + } + + return new { }; + } + + protected Common.Http.HttpResponse OAuthGet(HttpRequestBuilder builder) + { + var auth = OAuthRequest.ForProtectedResource(builder.Method.ToString(), Settings.ConsumerKey, Settings.ConsumerSecret, Settings.AccessToken, Settings.AccessTokenSecret); + + var request = builder.Build(); + request.LogResponseContent = true; + + // we need the url without the query to sign + auth.RequestUrl = request.Url.SetQuery(null).FullUri; + + var header = auth.GetAuthorizationHeader(builder.QueryParams.ToDictionary(x => x.Key, x => x.Value)); + request.Headers.Add("Authorization", header); + return _httpClient.Get(request); + } + + private NameValueCollection OAuthQuery(OAuthRequest oAuthRequest) + { + var auth = oAuthRequest.GetAuthorizationHeader(); + var request = new Common.Http.HttpRequest(oAuthRequest.RequestUrl); + request.Headers.Add("Authorization", auth); + var response = _httpClient.Get(request); + + return HttpUtility.ParseQueryString(response.Content); + } + + private Tuple GetUser() + { + var builder = RequestBuilder() + .SetSegment("route", $"api/auth_user") + .AddQueryParam("key", Settings.ConsumerKey, true); + + var httpResponse = OAuthGet(builder); + + string userId = null; + string userName = null; + + var content = httpResponse.Content; + + if (!string.IsNullOrWhiteSpace(content)) + { + var user = XDocument.Parse(content).XPathSelectElement("GoodreadsResponse/user"); + userId = user.AttributeAsString("id"); + userName = user.ElementAsString("name"); + } + + return Tuple.Create(userId, userName); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsOwnedBooks.cs b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsOwnedBooks.cs new file mode 100644 index 000000000..54c9002e1 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsOwnedBooks.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.MetadataSource.Goodreads; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.ImportLists.Goodreads +{ + public class GoodreadsOwnedBooksSettings : GoodreadsSettingsBase + { + } + + public class GoodreadsOwnedBooks : GoodreadsImportListBase + { + public GoodreadsOwnedBooks(IImportListStatusService importListStatusService, + IConfigService configService, + IParsingService parsingService, + IHttpClient httpClient, + Logger logger) + : base(importListStatusService, configService, parsingService, httpClient, logger) + { + } + + public override string Name => "Goodreads Owned Books"; + + public override IList Fetch() + { + var reviews = new List(); + var page = 0; + + while (true) + { + var curr = GetOwned(++page); + + if (curr == null || curr.Count == 0) + { + break; + } + + reviews.AddRange(curr); + } + + var result = reviews.Select(x => new ImportListItemInfo + { + Artist = x.Book.Authors.First().Name.CleanSpaces(), + ArtistMusicBrainzId = x.Book.Authors.First().Id.ToString(), + Album = x.Book.TitleWithoutSeries.CleanSpaces(), + AlbumMusicBrainzId = x.Book.Id.ToString() + }).ToList(); + + return CleanupListItems(result); + } + + private IReadOnlyList GetOwned(int page) + { + try + { + var builder = RequestBuilder() + .SetSegment("route", $"owned_books/user") + .AddQueryParam("id", Settings.UserId) + .AddQueryParam("page", page); + + var httpResponse = OAuthGet(builder); + + _logger.Trace("Got:\n{0}", httpResponse.Content); + + return httpResponse.Deserialize>("reviews").List; + } + catch (Exception ex) + { + _logger.Warn(ex, "Error fetching bookshelves from Goodreads"); + return new List(); + } + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsSettingsBase.cs b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsSettingsBase.cs new file mode 100644 index 000000000..87eb32d60 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Goodreads/GoodreadsSettingsBase.cs @@ -0,0 +1,58 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists.Goodreads +{ + public class GoodreadsSettingsBaseValidator : AbstractValidator + where TSettings : GoodreadsSettingsBase + { + public GoodreadsSettingsBaseValidator() + { + RuleFor(c => c.AccessToken).NotEmpty(); + RuleFor(c => c.AccessTokenSecret).NotEmpty(); + } + } + + public class GoodreadsSettingsBase : IImportListSettings + where TSettings : GoodreadsSettingsBase + { + public GoodreadsSettingsBase() + { + SignIn = "startOAuth"; + } + + public string BaseUrl { get; set; } + + public string ConsumerKey => "xQh8LhdTztb9u3cL26RqVg"; + public string ConsumerSecret => "96aDA1lJRcS8KofYbw2jjkRk3wTNKypHAL2GeOgbPZw"; + public string OAuthUrl => "https://www.goodreads.com/oauth/authorize"; + public string OAuthRequestTokenUrl => "https://www.goodreads.com/oauth/request_token"; + public string OAuthAccessTokenUrl => "https://www.goodreads.com/oauth/access_token"; + + [FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string AccessToken { get; set; } + + [FieldDefinition(0, Label = "Access Token Secret", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string AccessTokenSecret { get; set; } + + [FieldDefinition(0, Label = "Request Token Secret", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string RequestTokenSecret { get; set; } + + [FieldDefinition(0, Label = "User Id", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string UserId { get; set; } + + [FieldDefinition(0, Label = "User Name", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string UserName { get; set; } + + [FieldDefinition(99, Label = "Authenticate with Goodreads", Type = FieldType.OAuth)] + public string SignIn { get; set; } + + protected virtual AbstractValidator Validator => new GoodreadsSettingsBaseValidator(); + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate((TSettings)this)); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportApi.cs b/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportApi.cs deleted file mode 100644 index 4436bad2d..000000000 --- a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportApi.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.ImportLists.HeadphonesImport -{ - public class HeadphonesImportArtist - { - public string ArtistName { get; set; } - public string ArtistId { get; set; } - } -} diff --git a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportRequestGenerator.cs deleted file mode 100644 index 6f851332f..000000000 --- a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportRequestGenerator.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Http; - -namespace NzbDrone.Core.ImportLists.HeadphonesImport -{ - public class HeadphonesImportRequestGenerator : IImportListRequestGenerator - { - public HeadphonesImportSettings Settings { get; set; } - - public int MaxPages { get; set; } - public int PageSize { get; set; } - - public HeadphonesImportRequestGenerator() - { - MaxPages = 1; - PageSize = 1000; - } - - public virtual ImportListPageableRequestChain GetListItems() - { - var pageableRequests = new ImportListPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests()); - - return pageableRequests; - } - - private IEnumerable GetPagedRequests() - { - yield return new ImportListRequest(string.Format("{0}/api?cmd=getIndex&apikey={1}", Settings.BaseUrl.TrimEnd('/'), Settings.ApiKey), HttpAccept.Json); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportSettings.cs b/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportSettings.cs deleted file mode 100644 index 456b10399..000000000 --- a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportSettings.cs +++ /dev/null @@ -1,35 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.ImportLists.HeadphonesImport -{ - public class HeadphonesImportSettingsValidator : AbstractValidator - { - public HeadphonesImportSettingsValidator() - { - RuleFor(c => c.BaseUrl).ValidRootUrl(); - } - } - - public class HeadphonesImportSettings : IImportListSettings - { - private static readonly HeadphonesImportSettingsValidator Validator = new HeadphonesImportSettingsValidator(); - - public HeadphonesImportSettings() - { - BaseUrl = "http://localhost:8181/"; - } - - [FieldDefinition(0, Label = "Headphones URL")] - public string BaseUrl { get; set; } - - [FieldDefinition(1, Label = "API Key")] - public string ApiKey { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncCompleteEvent.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncCompleteEvent.cs index b055a0383..e84d61d43 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncCompleteEvent.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncCompleteEvent.cs @@ -6,9 +6,9 @@ namespace NzbDrone.Core.ImportLists { public class ImportListSyncCompleteEvent : IEvent { - public List ProcessedDecisions { get; private set; } + public List ProcessedDecisions { get; private set; } - public ImportListSyncCompleteEvent(List processedDecisions) + public ImportListSyncCompleteEvent(List processedDecisions) { ProcessedDecisions = processedDecisions; } diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 058dfd7ea..5b8ef4aca 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Commands; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.ImportLists @@ -17,25 +18,27 @@ namespace NzbDrone.Core.ImportLists private readonly IImportListFactory _importListFactory; private readonly IImportListExclusionService _importListExclusionService; private readonly IFetchAndParseImportList _listFetcherAndParser; - private readonly ISearchForNewAlbum _albumSearchService; - private readonly ISearchForNewArtist _artistSearchService; + private readonly ISearchForNewBook _albumSearchService; + private readonly ISearchForNewAuthor _artistSearchService; private readonly IArtistService _artistService; private readonly IAlbumService _albumService; private readonly IAddArtistService _addArtistService; private readonly IAddAlbumService _addAlbumService; private readonly IEventAggregator _eventAggregator; + private readonly IManageCommandQueue _commandQueueManager; private readonly Logger _logger; public ImportListSyncService(IImportListFactory importListFactory, IImportListExclusionService importListExclusionService, IFetchAndParseImportList listFetcherAndParser, - ISearchForNewAlbum albumSearchService, - ISearchForNewArtist artistSearchService, + ISearchForNewBook albumSearchService, + ISearchForNewAuthor artistSearchService, IArtistService artistService, IAlbumService albumService, IAddArtistService addArtistService, IAddAlbumService addAlbumService, IEventAggregator eventAggregator, + IManageCommandQueue commandQueueManager, Logger logger) { _importListFactory = importListFactory; @@ -48,10 +51,11 @@ namespace NzbDrone.Core.ImportLists _addArtistService = addArtistService; _addAlbumService = addAlbumService; _eventAggregator = eventAggregator; + _commandQueueManager = commandQueueManager; _logger = logger; } - private List SyncAll() + private List SyncAll() { _logger.ProgressInfo("Starting Import List Sync"); @@ -62,7 +66,7 @@ namespace NzbDrone.Core.ImportLists return ProcessReports(reports); } - private List SyncList(ImportListDefinition definition) + private List SyncList(ImportListDefinition definition) { _logger.ProgressInfo(string.Format("Starting Import List Refresh for List {0}", definition.Name)); @@ -73,11 +77,11 @@ namespace NzbDrone.Core.ImportLists return ProcessReports(reports); } - private List ProcessReports(List reports) + private List ProcessReports(List reports) { - var processed = new List(); - var artistsToAdd = new List(); - var albumsToAdd = new List(); + var processed = new List(); + var artistsToAdd = new List(); + var albumsToAdd = new List(); _logger.ProgressInfo("Processing {0} list items", reports.Count); @@ -113,35 +117,52 @@ namespace NzbDrone.Core.ImportLists } } - _addArtistService.AddArtists(artistsToAdd); - _addAlbumService.AddAlbums(albumsToAdd); + var addedArtists = _addArtistService.AddArtists(artistsToAdd, false); + var addedAlbums = _addAlbumService.AddAlbums(albumsToAdd, false); var message = string.Format($"Import List Sync Completed. Items found: {reports.Count}, Artists added: {artistsToAdd.Count}, Albums added: {albumsToAdd.Count}"); _logger.ProgressInfo(message); + var toRefresh = addedArtists.Select(x => x.Id).Concat(addedAlbums.Select(x => x.Author.Value.Id)).Distinct().ToList(); + if (toRefresh.Any()) + { + _commandQueueManager.Push(new BulkRefreshArtistCommand(toRefresh, true)); + } + return processed; } private void MapAlbumReport(ImportListItemInfo report) { - var albumQuery = report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() ? $"readarr:{report.AlbumMusicBrainzId}" : report.Album; - var mappedAlbum = _albumSearchService.SearchForNewAlbum(albumQuery, report.Artist) - .FirstOrDefault(); + Book mappedAlbum; + + if (report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() && int.TryParse(report.AlbumMusicBrainzId, out var goodreadsId)) + { + mappedAlbum = _albumSearchService.SearchByGoodreadsId(goodreadsId).FirstOrDefault(x => x.GoodreadsId == goodreadsId); + } + else + { + mappedAlbum = _albumSearchService.SearchForNewBook(report.Album, report.Artist).FirstOrDefault(); + } // Break if we are looking for an album and cant find it. This will avoid us from adding the artist and possibly getting it wrong. if (mappedAlbum == null) { + _logger.Trace($"Nothing found for {report.AlbumMusicBrainzId}"); + report.AlbumMusicBrainzId = null; return; } - report.AlbumMusicBrainzId = mappedAlbum.ForeignAlbumId; + _logger.Trace($"Mapped {report.AlbumMusicBrainzId} to {mappedAlbum}"); + + report.AlbumMusicBrainzId = mappedAlbum.ForeignBookId; report.Album = mappedAlbum.Title; - report.Artist = mappedAlbum.ArtistMetadata?.Value?.Name; - report.ArtistMusicBrainzId = mappedAlbum.ArtistMetadata?.Value?.ForeignArtistId; + report.Artist = mappedAlbum.AuthorMetadata?.Value?.Name; + report.ArtistMusicBrainzId = mappedAlbum.AuthorMetadata?.Value?.ForeignAuthorId; } - private void ProcessAlbumReport(ImportListDefinition importList, ImportListItemInfo report, List listExclusions, List albumsToAdd) + private void ProcessAlbumReport(ImportListDefinition importList, ImportListItemInfo report, List listExclusions, List albumsToAdd) { if (report.AlbumMusicBrainzId == null) { @@ -176,23 +197,21 @@ namespace NzbDrone.Core.ImportLists } // Append Album if not already in DB or already on add list - if (albumsToAdd.All(s => s.ForeignAlbumId != report.AlbumMusicBrainzId)) + if (albumsToAdd.All(s => s.ForeignBookId != report.AlbumMusicBrainzId)) { var monitored = importList.ShouldMonitor != ImportListMonitorType.None; - var toAdd = new Album + var toAdd = new Book { - ForeignAlbumId = report.AlbumMusicBrainzId, + ForeignBookId = report.AlbumMusicBrainzId, Monitored = monitored, - AnyReleaseOk = true, - Artist = new Artist + Author = new Author { Monitored = monitored, RootFolderPath = importList.RootFolderPath, QualityProfileId = importList.ProfileId, MetadataProfileId = importList.MetadataProfileId, Tags = importList.Tags, - AlbumFolder = true, AddOptions = new AddArtistOptions { SearchForMissingAlbums = monitored, @@ -204,7 +223,7 @@ namespace NzbDrone.Core.ImportLists if (importList.ShouldMonitor == ImportListMonitorType.SpecificAlbum) { - toAdd.Artist.Value.AddOptions.AlbumsToMonitor.Add(toAdd.ForeignAlbumId); + toAdd.Author.Value.AddOptions.AlbumsToMonitor.Add(toAdd.ForeignBookId); } albumsToAdd.Add(toAdd); @@ -213,13 +232,13 @@ namespace NzbDrone.Core.ImportLists private void MapArtistReport(ImportListItemInfo report) { - var mappedArtist = _artistSearchService.SearchForNewArtist(report.Artist) + var mappedArtist = _artistSearchService.SearchForNewAuthor(report.Artist) .FirstOrDefault(); - report.ArtistMusicBrainzId = mappedArtist?.Metadata.Value?.ForeignArtistId; + report.ArtistMusicBrainzId = mappedArtist?.Metadata.Value?.ForeignAuthorId; report.Artist = mappedArtist?.Metadata.Value?.Name; } - private void ProcessArtistReport(ImportListDefinition importList, ImportListItemInfo report, List listExclusions, List artistsToAdd) + private void ProcessArtistReport(ImportListDefinition importList, ImportListItemInfo report, List listExclusions, List artistsToAdd) { if (report.ArtistMusicBrainzId == null) { @@ -245,15 +264,15 @@ namespace NzbDrone.Core.ImportLists } // Append Artist if not already in DB or already on add list - if (artistsToAdd.All(s => s.Metadata.Value.ForeignArtistId != report.ArtistMusicBrainzId)) + if (artistsToAdd.All(s => s.Metadata.Value.ForeignAuthorId != report.ArtistMusicBrainzId)) { var monitored = importList.ShouldMonitor != ImportListMonitorType.None; - artistsToAdd.Add(new Artist + artistsToAdd.Add(new Author { - Metadata = new ArtistMetadata + Metadata = new AuthorMetadata { - ForeignArtistId = report.ArtistMusicBrainzId, + ForeignAuthorId = report.ArtistMusicBrainzId, Name = report.Artist }, Monitored = monitored, @@ -261,7 +280,6 @@ namespace NzbDrone.Core.ImportLists QualityProfileId = importList.ProfileId, MetadataProfileId = importList.MetadataProfileId, Tags = importList.Tags, - AlbumFolder = true, AddOptions = new AddArtistOptions { SearchForMissingAlbums = monitored, @@ -274,7 +292,7 @@ namespace NzbDrone.Core.ImportLists public void Execute(ImportListSyncCommand message) { - List processed; + List processed; if (message.DefinitionId.HasValue) { diff --git a/src/NzbDrone.Core/ImportLists/ImportListType.cs b/src/NzbDrone.Core/ImportLists/ImportListType.cs index 36aa04376..6e3424d3b 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListType.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListType.cs @@ -2,8 +2,7 @@ namespace NzbDrone.Core.ImportLists { public enum ImportListType { - Spotify, - LastFm, + Goodreads, Other } } diff --git a/src/NzbDrone.Core/ImportLists/LastFm/LastFmApi.cs b/src/NzbDrone.Core/ImportLists/LastFm/LastFmApi.cs deleted file mode 100644 index b52c9f710..000000000 --- a/src/NzbDrone.Core/ImportLists/LastFm/LastFmApi.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.ImportLists.LastFm -{ - public class LastFmArtistList - { - public List Artist { get; set; } - } - - public class LastFmArtistResponse - { - public LastFmArtistList TopArtists { get; set; } - } - - public class LastFmArtist - { - public string Name { get; set; } - public string Mbid { get; set; } - } -} diff --git a/src/NzbDrone.Core/ImportLists/LastFm/LastFmParser.cs b/src/NzbDrone.Core/ImportLists/LastFm/LastFmParser.cs deleted file mode 100644 index fa60b0fae..000000000 --- a/src/NzbDrone.Core/ImportLists/LastFm/LastFmParser.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.ImportLists.Exceptions; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.ImportLists.LastFm -{ - public class LastFmParser : IParseImportListResponse - { - private ImportListResponse _importListResponse; - - public IList ParseResponse(ImportListResponse importListResponse) - { - _importListResponse = importListResponse; - - var items = new List(); - - if (!PreProcess(_importListResponse)) - { - return items; - } - - var jsonResponse = Json.Deserialize(_importListResponse.Content); - - if (jsonResponse == null) - { - return items; - } - - foreach (var item in jsonResponse.TopArtists.Artist) - { - items.AddIfNotNull(new ImportListItemInfo - { - Artist = item.Name, - ArtistMusicBrainzId = item.Mbid - }); - } - - return items; - } - - protected virtual bool PreProcess(ImportListResponse importListResponse) - { - if (importListResponse.HttpResponse.StatusCode != HttpStatusCode.OK) - { - throw new ImportListException(importListResponse, "Import List API call resulted in an unexpected StatusCode [{0}]", importListResponse.HttpResponse.StatusCode); - } - - if (importListResponse.HttpResponse.Headers.ContentType != null && importListResponse.HttpResponse.Headers.ContentType.Contains("text/json") && - importListResponse.HttpRequest.Headers.Accept != null && !importListResponse.HttpRequest.Headers.Accept.Contains("text/json")) - { - throw new ImportListException(importListResponse, "Import List responded with html content. Site is likely blocked or unavailable."); - } - - return true; - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/LastFm/LastFmTag.cs b/src/NzbDrone.Core/ImportLists/LastFm/LastFmTag.cs deleted file mode 100644 index 727c3d1dc..000000000 --- a/src/NzbDrone.Core/ImportLists/LastFm/LastFmTag.cs +++ /dev/null @@ -1,31 +0,0 @@ -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.ImportLists.LastFm -{ - public class LastFmTag : HttpImportListBase - { - public override string Name => "Last.fm Tag"; - - public override ImportListType ListType => ImportListType.LastFm; - - public override int PageSize => 1000; - - public LastFmTag(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) - { - } - - public override IImportListRequestGenerator GetRequestGenerator() - { - return new LastFmTagRequestGenerator { Settings = Settings }; - } - - public override IParseImportListResponse GetParser() - { - return new LastFmParser(); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/LastFm/LastFmTagSettings.cs b/src/NzbDrone.Core/ImportLists/LastFm/LastFmTagSettings.cs deleted file mode 100644 index b9a4b41bc..000000000 --- a/src/NzbDrone.Core/ImportLists/LastFm/LastFmTagSettings.cs +++ /dev/null @@ -1,41 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.ImportLists.LastFm -{ - public class LastFmTagSettingsValidator : AbstractValidator - { - public LastFmTagSettingsValidator() - { - RuleFor(c => c.TagId).NotEmpty(); - RuleFor(c => c.Count).LessThanOrEqualTo(1000); - } - } - - public class LastFmTagSettings : IImportListSettings - { - private static readonly LastFmTagSettingsValidator Validator = new LastFmTagSettingsValidator(); - - public LastFmTagSettings() - { - BaseUrl = "http://ws.audioscrobbler.com/2.0/?method=tag.gettopartists"; - ApiKey = "204c76646d6020eee36bbc51a2fcd810"; - Count = 25; - } - - public string BaseUrl { get; set; } - public string ApiKey { get; set; } - - [FieldDefinition(0, Label = "Last.fm Tag", HelpText = "Tag to pull artists from")] - public string TagId { get; set; } - - [FieldDefinition(1, Label = "Count", HelpText = "Number of results to pull from list (Max 1000)", Type = FieldType.Number)] - public int Count { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/LastFm/LastFmUser.cs b/src/NzbDrone.Core/ImportLists/LastFm/LastFmUser.cs deleted file mode 100644 index 884062aa7..000000000 --- a/src/NzbDrone.Core/ImportLists/LastFm/LastFmUser.cs +++ /dev/null @@ -1,31 +0,0 @@ -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.ImportLists.LastFm -{ - public class LastFmUser : HttpImportListBase - { - public override string Name => "Last.fm User"; - - public override ImportListType ListType => ImportListType.LastFm; - - public override int PageSize => 1000; - - public LastFmUser(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) - { - } - - public override IImportListRequestGenerator GetRequestGenerator() - { - return new LastFmUserRequestGenerator { Settings = Settings }; - } - - public override IParseImportListResponse GetParser() - { - return new LastFmParser(); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/LastFm/LastFmUserRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/LastFm/LastFmUserRequestGenerator.cs deleted file mode 100644 index 5bdfb7a07..000000000 --- a/src/NzbDrone.Core/ImportLists/LastFm/LastFmUserRequestGenerator.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Http; - -namespace NzbDrone.Core.ImportLists.LastFm -{ - public class LastFmUserRequestGenerator : IImportListRequestGenerator - { - public LastFmUserSettings Settings { get; set; } - - public int MaxPages { get; set; } - public int PageSize { get; set; } - - public LastFmUserRequestGenerator() - { - MaxPages = 1; - PageSize = 1000; - } - - public virtual ImportListPageableRequestChain GetListItems() - { - var pageableRequests = new ImportListPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests()); - - return pageableRequests; - } - - private IEnumerable GetPagedRequests() - { - yield return new ImportListRequest(string.Format("{0}&user={1}&limit={2}&api_key={3}&format=json", Settings.BaseUrl.TrimEnd('/'), Settings.UserId, Settings.Count, Settings.ApiKey), HttpAccept.Json); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/LastFm/LastFmUserSettings.cs b/src/NzbDrone.Core/ImportLists/LastFm/LastFmUserSettings.cs deleted file mode 100644 index 07d7ad41c..000000000 --- a/src/NzbDrone.Core/ImportLists/LastFm/LastFmUserSettings.cs +++ /dev/null @@ -1,41 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.ImportLists.LastFm -{ - public class LastFmSettingsValidator : AbstractValidator - { - public LastFmSettingsValidator() - { - RuleFor(c => c.UserId).NotEmpty(); - RuleFor(c => c.Count).LessThanOrEqualTo(1000); - } - } - - public class LastFmUserSettings : IImportListSettings - { - private static readonly LastFmSettingsValidator Validator = new LastFmSettingsValidator(); - - public LastFmUserSettings() - { - BaseUrl = "http://ws.audioscrobbler.com/2.0/?method=user.gettopartists"; - ApiKey = "204c76646d6020eee36bbc51a2fcd810"; - Count = 25; - } - - public string BaseUrl { get; set; } - public string ApiKey { get; set; } - - [FieldDefinition(0, Label = "Last.fm UserID", HelpText = "Last.fm UserId to pull artists from")] - public string UserId { get; set; } - - [FieldDefinition(1, Label = "Count", HelpText = "Number of results to pull from list (Max 1000)", Type = FieldType.Number)] - public int Count { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImport.cs b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImport.cs similarity index 50% rename from src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImport.cs rename to src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImport.cs index f587649b4..c56825a6c 100644 --- a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImport.cs +++ b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImport.cs @@ -3,29 +3,29 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Parser; -namespace NzbDrone.Core.ImportLists.HeadphonesImport +namespace NzbDrone.Core.ImportLists.LazyLibrarianImport { - public class HeadphonesImport : HttpImportListBase + public class LazyLibrarianImport : HttpImportListBase { - public override string Name => "Headphones"; + public override string Name => "LazyLibrarian"; public override ImportListType ListType => ImportListType.Other; public override int PageSize => 1000; - public HeadphonesImport(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger) + public LazyLibrarianImport(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger) : base(httpClient, importListStatusService, configService, parsingService, logger) { } public override IImportListRequestGenerator GetRequestGenerator() { - return new HeadphonesImportRequestGenerator { Settings = Settings }; + return new LazyLibrarianImportRequestGenerator { Settings = Settings }; } public override IParseImportListResponse GetParser() { - return new HeadphonesImportParser(); + return new LazyLibrarianImportParser(); } } } diff --git a/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportApi.cs b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportApi.cs new file mode 100644 index 000000000..3e3e585ff --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportApi.cs @@ -0,0 +1,11 @@ +namespace NzbDrone.Core.ImportLists.LazyLibrarianImport +{ + public class LazyLibrarianBook + { + public string BookName { get; set; } + public string BookId { get; set; } + public string BookIsbn { get; set; } + public string AuthorName { get; set; } + public string AuthorId { get; set; } + } +} diff --git a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportParser.cs b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportParser.cs similarity index 85% rename from src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportParser.cs rename to src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportParser.cs index 1fbe46530..06dd86477 100644 --- a/src/NzbDrone.Core/ImportLists/HeadphonesImport/HeadphonesImportParser.cs +++ b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportParser.cs @@ -5,9 +5,9 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.ImportLists.Exceptions; using NzbDrone.Core.Parser.Model; -namespace NzbDrone.Core.ImportLists.HeadphonesImport +namespace NzbDrone.Core.ImportLists.LazyLibrarianImport { - public class HeadphonesImportParser : IParseImportListResponse + public class LazyLibrarianImportParser : IParseImportListResponse { private ImportListResponse _importListResponse; @@ -22,7 +22,7 @@ namespace NzbDrone.Core.ImportLists.HeadphonesImport return items; } - var jsonResponse = JsonConvert.DeserializeObject>(_importListResponse.Content); + var jsonResponse = JsonConvert.DeserializeObject>(_importListResponse.Content); // no albums were return if (jsonResponse == null) @@ -34,8 +34,9 @@ namespace NzbDrone.Core.ImportLists.HeadphonesImport { items.AddIfNotNull(new ImportListItemInfo { - Artist = item.ArtistName, - ArtistMusicBrainzId = item.ArtistId + Artist = item.AuthorName, + Album = item.BookName, + AlbumMusicBrainzId = item.BookId }); } diff --git a/src/NzbDrone.Core/ImportLists/LastFm/LastFmTagRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportRequestGenerator.cs similarity index 64% rename from src/NzbDrone.Core/ImportLists/LastFm/LastFmTagRequestGenerator.cs rename to src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportRequestGenerator.cs index bfd662984..79f9b2520 100644 --- a/src/NzbDrone.Core/ImportLists/LastFm/LastFmTagRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportRequestGenerator.cs @@ -1,16 +1,16 @@ using System.Collections.Generic; using NzbDrone.Common.Http; -namespace NzbDrone.Core.ImportLists.LastFm +namespace NzbDrone.Core.ImportLists.LazyLibrarianImport { - public class LastFmTagRequestGenerator : IImportListRequestGenerator + public class LazyLibrarianImportRequestGenerator : IImportListRequestGenerator { - public LastFmTagSettings Settings { get; set; } + public LazyLibrarianImportSettings Settings { get; set; } public int MaxPages { get; set; } public int PageSize { get; set; } - public LastFmTagRequestGenerator() + public LazyLibrarianImportRequestGenerator() { MaxPages = 1; PageSize = 1000; @@ -27,7 +27,7 @@ namespace NzbDrone.Core.ImportLists.LastFm private IEnumerable GetPagedRequests() { - yield return new ImportListRequest(string.Format("{0}&tag={1}&limit={2}&api_key={3}&format=json", Settings.BaseUrl.TrimEnd('/'), Settings.TagId, Settings.Count, Settings.ApiKey), HttpAccept.Json); + yield return new ImportListRequest(string.Format("{0}/api?cmd=getAllBooks&apikey={1}", Settings.BaseUrl.TrimEnd('/'), Settings.ApiKey), HttpAccept.Json); } } } diff --git a/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportSettings.cs b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportSettings.cs new file mode 100644 index 000000000..6bcbb6219 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/LazyLibrarian/LazyLibrarianImportSettings.cs @@ -0,0 +1,36 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists.LazyLibrarianImport +{ + public class LazyLibrarianImportSettingsValidator : AbstractValidator + { + public LazyLibrarianImportSettingsValidator() + { + RuleFor(c => c.BaseUrl).IsValidUrl(); + RuleFor(c => c.ApiKey).NotEmpty(); + } + } + + public class LazyLibrarianImportSettings : IImportListSettings + { + private static readonly LazyLibrarianImportSettingsValidator Validator = new LazyLibrarianImportSettingsValidator(); + + public LazyLibrarianImportSettings() + { + BaseUrl = "http://localhost:5299"; + } + + [FieldDefinition(0, Label = "Url")] + public string BaseUrl { get; set; } + + [FieldDefinition(1, Label = "API Key")] + public string ApiKey { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrLists.cs b/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrLists.cs deleted file mode 100644 index 0c7c4021c..000000000 --- a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrLists.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Parser; -using NzbDrone.Core.ThingiProvider; - -namespace NzbDrone.Core.ImportLists.ReadarrLists -{ - public class ReadarrLists : HttpImportListBase - { - public override string Name => "Readarr Lists"; - - public override ImportListType ListType => ImportListType.Other; - - public override int PageSize => 10; - - private readonly IMetadataRequestBuilder _requestBuilder; - - public ReadarrLists(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, IMetadataRequestBuilder requestBuilder, Logger logger) - : base(httpClient, importListStatusService, configService, parsingService, logger) - { - _requestBuilder = requestBuilder; - } - - public override IEnumerable DefaultDefinitions - { - get - { - yield return GetDefinition("iTunes Top Albums", GetSettings("itunes/album/top")); - yield return GetDefinition("iTunes New Albums", GetSettings("itunes/album/new")); - yield return GetDefinition("Apple Music Top Albums", GetSettings("apple-music/album/top")); - yield return GetDefinition("Apple Music New Albums", GetSettings("apple-music/album/new")); - yield return GetDefinition("Billboard Top Albums", GetSettings("billboard/album/top")); - yield return GetDefinition("Billboard Top Artists", GetSettings("billboard/artist/top")); - yield return GetDefinition("Last.fm Top Artists", GetSettings("lastfm/artist/top")); - } - } - - private ImportListDefinition GetDefinition(string name, ReadarrListsSettings settings) - { - return new ImportListDefinition - { - EnableAutomaticAdd = false, - Name = name, - Implementation = GetType().Name, - Settings = settings - }; - } - - private ReadarrListsSettings GetSettings(string url) - { - var settings = new ReadarrListsSettings { ListId = url }; - - return settings; - } - - public override IImportListRequestGenerator GetRequestGenerator() - { - return new ReadarrListsRequestGenerator(_requestBuilder) { Settings = Settings }; - } - - public override IParseImportListResponse GetParser() - { - return new ReadarrListsParser(Settings); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsApi.cs b/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsApi.cs deleted file mode 100644 index a822ab1b2..000000000 --- a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsApi.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace NzbDrone.Core.ImportLists.ReadarrLists -{ - public class ReadarrListsAlbum - { - public string ArtistName { get; set; } - public string AlbumTitle { get; set; } - public string ArtistId { get; set; } - public string AlbumId { get; set; } - public DateTime? ReleaseDate { get; set; } - } -} diff --git a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsParser.cs b/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsParser.cs deleted file mode 100644 index bd9e17b9e..000000000 --- a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsParser.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using Newtonsoft.Json; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.ImportLists.Exceptions; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.ImportLists.ReadarrLists -{ - public class ReadarrListsParser : IParseImportListResponse - { - private readonly ReadarrListsSettings _settings; - private ImportListResponse _importListResponse; - - public ReadarrListsParser(ReadarrListsSettings settings) - { - _settings = settings; - } - - public IList ParseResponse(ImportListResponse importListResponse) - { - _importListResponse = importListResponse; - - var items = new List(); - - if (!PreProcess(_importListResponse)) - { - return items; - } - - var jsonResponse = JsonConvert.DeserializeObject>(_importListResponse.Content); - - // no albums were return - if (jsonResponse == null) - { - return items; - } - - foreach (var item in jsonResponse) - { - items.AddIfNotNull(new ImportListItemInfo - { - Artist = item.ArtistName, - Album = item.AlbumTitle, - ArtistMusicBrainzId = item.ArtistId, - AlbumMusicBrainzId = item.AlbumId, - ReleaseDate = item.ReleaseDate.GetValueOrDefault() - }); - } - - return items; - } - - protected virtual bool PreProcess(ImportListResponse importListResponse) - { - if (importListResponse.HttpResponse.StatusCode != HttpStatusCode.OK) - { - throw new ImportListException(importListResponse, "Import List API call resulted in an unexpected StatusCode [{0}]", importListResponse.HttpResponse.StatusCode); - } - - if (importListResponse.HttpResponse.Headers.ContentType != null && importListResponse.HttpResponse.Headers.ContentType.Contains("text/json") && - importListResponse.HttpRequest.Headers.Accept != null && !importListResponse.HttpRequest.Headers.Accept.Contains("text/json")) - { - throw new ImportListException(importListResponse, "Import List responded with html content. Site is likely blocked or unavailable."); - } - - return true; - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsRequestGenerator.cs deleted file mode 100644 index a07e219ba..000000000 --- a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsRequestGenerator.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.MetadataSource; - -namespace NzbDrone.Core.ImportLists.ReadarrLists -{ - public class ReadarrListsRequestGenerator : IImportListRequestGenerator - { - public ReadarrListsSettings Settings { get; set; } - - private readonly IMetadataRequestBuilder _requestBulder; - - public ReadarrListsRequestGenerator(IMetadataRequestBuilder requestBuilder) - { - _requestBulder = requestBuilder; - } - - public virtual ImportListPageableRequestChain GetListItems() - { - var pageableRequests = new ImportListPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests()); - - return pageableRequests; - } - - private IEnumerable GetPagedRequests() - { - var request = _requestBulder.GetRequestBuilder() - .Create() - .SetSegment("route", "chart/" + Settings.ListId) - .Build(); - - yield return new ImportListRequest(request); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsSettings.cs b/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsSettings.cs deleted file mode 100644 index ef060853a..000000000 --- a/src/NzbDrone.Core/ImportLists/ReadarrLists/ReadarrListsSettings.cs +++ /dev/null @@ -1,33 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.ImportLists.ReadarrLists -{ - public class ReadarrListsSettingsValidator : AbstractValidator - { - public ReadarrListsSettingsValidator() - { - } - } - - public class ReadarrListsSettings : IImportListSettings - { - private static readonly ReadarrListsSettingsValidator Validator = new ReadarrListsSettingsValidator(); - - public ReadarrListsSettings() - { - BaseUrl = ""; - } - - public string BaseUrl { get; set; } - - [FieldDefinition(0, Label = "List Id", Advanced = true)] - public string ListId { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyException.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyException.cs deleted file mode 100644 index a9aae6894..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyException.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using NzbDrone.Common.Exceptions; - -namespace NzbDrone.Core.ImportLists.Spotify -{ - public class SpotifyException : NzbDroneException - { - public SpotifyException(string message) - : base(message) - { - } - - public SpotifyException(string message, params object[] args) - : base(message, args) - { - } - - public SpotifyException(string message, Exception innerException) - : base(message, innerException) - { - } - } - - public class SpotifyAuthorizationException : SpotifyException - { - public SpotifyAuthorizationException(string message) - : base(message) - { - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyFollowedArtists.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyFollowedArtists.cs deleted file mode 100644 index 414c05d67..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyFollowedArtists.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Generic; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Parser; -using SpotifyAPI.Web; -using SpotifyAPI.Web.Models; - -namespace NzbDrone.Core.ImportLists.Spotify -{ - public class SpotifyFollowedArtistsSettings : SpotifySettingsBase - { - public override string Scope => "user-follow-read"; - } - - public class SpotifyFollowedArtists : SpotifyImportListBase - { - public SpotifyFollowedArtists(ISpotifyProxy spotifyProxy, - IMetadataRequestBuilder requestBuilder, - IImportListStatusService importListStatusService, - IImportListRepository importListRepository, - IConfigService configService, - IParsingService parsingService, - IHttpClient httpClient, - Logger logger) - : base(spotifyProxy, requestBuilder, importListStatusService, importListRepository, configService, parsingService, httpClient, logger) - { - } - - public override string Name => "Spotify Followed Artists"; - - public override IList Fetch(SpotifyWebAPI api) - { - var result = new List(); - - var followedArtists = _spotifyProxy.GetFollowedArtists(this, api); - var artists = followedArtists?.Artists; - - while (true) - { - if (artists?.Items == null) - { - return result; - } - - foreach (var artist in artists.Items) - { - result.AddIfNotNull(ParseFullArtist(artist)); - } - - if (!artists.HasNext()) - { - break; - } - - followedArtists = _spotifyProxy.GetNextPage(this, api, followedArtists); - artists = followedArtists?.Artists; - } - - return result; - } - - private SpotifyImportListItemInfo ParseFullArtist(FullArtist artist) - { - if (artist?.Name.IsNotNullOrWhiteSpace() ?? false) - { - return new SpotifyImportListItemInfo - { - Artist = artist.Name, - ArtistSpotifyId = artist.Id - }; - } - - return null; - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs deleted file mode 100644 index 57722ea44..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs +++ /dev/null @@ -1,354 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using FluentValidation.Results; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.MetadataSource.SkyHook.Resource; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Validation; -using SpotifyAPI.Web; -using SpotifyAPI.Web.Models; - -namespace NzbDrone.Core.ImportLists.Spotify -{ - public abstract class SpotifyImportListBase : ImportListBase - where TSettings : SpotifySettingsBase, new() - { - private IHttpClient _httpClient; - private IImportListRepository _importListRepository; - - protected ISpotifyProxy _spotifyProxy; - private readonly IMetadataRequestBuilder _requestBuilder; - - protected SpotifyImportListBase(ISpotifyProxy spotifyProxy, - IMetadataRequestBuilder requestBuilder, - IImportListStatusService importListStatusService, - IImportListRepository importListRepository, - IConfigService configService, - IParsingService parsingService, - IHttpClient httpClient, - Logger logger) - : base(importListStatusService, configService, parsingService, logger) - { - _httpClient = httpClient; - _importListRepository = importListRepository; - _spotifyProxy = spotifyProxy; - _requestBuilder = requestBuilder; - } - - public override ImportListType ListType => ImportListType.Spotify; - - public string AccessToken => Settings.AccessToken; - - public void RefreshToken() - { - _logger.Trace("Refreshing Token"); - - Settings.Validate().Filter("RefreshToken").ThrowOnError(); - - var request = new HttpRequestBuilder(Settings.RenewUri) - .AddQueryParam("refresh_token", Settings.RefreshToken) - .Build(); - - try - { - var response = _httpClient.Get(request); - - if (response != null && response.Resource != null) - { - var token = response.Resource; - Settings.AccessToken = token.AccessToken; - Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn); - Settings.RefreshToken = token.RefreshToken != null ? token.RefreshToken : Settings.RefreshToken; - - if (Definition.Id > 0) - { - _importListRepository.UpdateSettings((ImportListDefinition)Definition); - } - } - } - catch (HttpException) - { - _logger.Warn($"Error refreshing spotify access token"); - } - } - - public SpotifyWebAPI GetApi() - { - Settings.Validate().Filter("AccessToken", "RefreshToken").ThrowOnError(); - _logger.Trace($"Access token expires at {Settings.Expires}"); - - if (Settings.Expires < DateTime.UtcNow.AddMinutes(5)) - { - RefreshToken(); - } - - return new SpotifyWebAPI - { - AccessToken = Settings.AccessToken, - TokenType = "Bearer" - }; - } - - public override IList Fetch() - { - IList releases; - using (var api = GetApi()) - { - _logger.Debug("Starting spotify import list sync"); - releases = Fetch(api); - } - - // map to musicbrainz ids - releases = MapSpotifyReleases(releases); - - return CleanupListItems(releases); - } - - public abstract IList Fetch(SpotifyWebAPI api); - - protected DateTime ParseSpotifyDate(string date, string precision) - { - if (date.IsNullOrWhiteSpace() || precision.IsNullOrWhiteSpace()) - { - return default(DateTime); - } - - string format; - - switch (precision) - { - case "year": - format = "yyyy"; - break; - case "month": - format = "yyyy-MM"; - break; - case "day": - default: - format = "yyyy-MM-dd"; - break; - } - - return DateTime.TryParseExact(date, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime result) ? result : default(DateTime); - } - - public IList MapSpotifyReleases(IList items) - { - // first pass bulk lookup, server won't do search - var spotifyIds = items.Select(x => x.ArtistSpotifyId) - .Concat(items.Select(x => x.AlbumSpotifyId)) - .Where(x => x.IsNotNullOrWhiteSpace()) - .Distinct(); - var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", "spotify/lookup") - .Build(); - httpRequest.SetContent(spotifyIds.ToJson()); - httpRequest.Headers.ContentType = "application/json"; - - _logger.Trace($"Requesting maps for:\n{spotifyIds.ToJson()}"); - - Dictionary map; - try - { - var httpResponse = _httpClient.Post>(httpRequest); - var mapList = httpResponse.Resource; - - // Generate a mapping dictionary. - // The API will return 0 to mean it has previously searched and can't find the item. - // null means that it has never been searched before. - map = mapList.Where(x => x.MusicbrainzId.IsNotNullOrWhiteSpace()) - .ToDictionary(x => x.SpotifyId, x => x.MusicbrainzId); - } - catch (Exception e) - { - _logger.Error(e); - map = new Dictionary(); - } - - _logger.Trace("Got mapping:\n{0}", map.ToJson()); - - foreach (var item in items) - { - if (item.AlbumSpotifyId.IsNotNullOrWhiteSpace()) - { - if (map.ContainsKey(item.AlbumSpotifyId)) - { - item.AlbumMusicBrainzId = map[item.AlbumSpotifyId]; - } - else - { - MapAlbumItem(item); - } - } - else if (item.ArtistSpotifyId.IsNotNullOrWhiteSpace()) - { - if (map.ContainsKey(item.ArtistSpotifyId)) - { - item.ArtistMusicBrainzId = map[item.ArtistSpotifyId]; - } - else - { - MapArtistItem(item); - } - } - } - - // Strip out items where mapped to not found - return items.Where(x => x.AlbumMusicBrainzId != "0" && x.ArtistMusicBrainzId != "0").ToList(); - } - - public void MapArtistItem(SpotifyImportListItemInfo item) - { - if (item.ArtistSpotifyId.IsNullOrWhiteSpace()) - { - return; - } - - var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", $"spotify/artist/{item.ArtistSpotifyId}") - .Build(); - httpRequest.AllowAutoRedirect = true; - httpRequest.SuppressHttpError = true; - - try - { - var response = _httpClient.Get(httpRequest); - - if (response.HasHttpError) - { - if (response.StatusCode == HttpStatusCode.NotFound) - { - item.ArtistMusicBrainzId = "0"; - return; - } - else - { - throw new HttpException(httpRequest, response); - } - } - - item.ArtistMusicBrainzId = response.Resource.Id; - } - catch (HttpException e) - { - _logger.Warn(e, "Unable to communicate with ReadarrAPI"); - } - catch (Exception e) - { - _logger.Error(e); - } - } - - public void MapAlbumItem(SpotifyImportListItemInfo item) - { - if (item.AlbumSpotifyId.IsNullOrWhiteSpace()) - { - return; - } - - var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", $"spotify/album/{item.AlbumSpotifyId}") - .Build(); - httpRequest.AllowAutoRedirect = true; - httpRequest.SuppressHttpError = true; - - try - { - var response = _httpClient.Get(httpRequest); - - if (response.HasHttpError) - { - if (response.StatusCode == HttpStatusCode.NotFound) - { - item.AlbumMusicBrainzId = "0"; - return; - } - else - { - throw new HttpException(httpRequest, response); - } - } - - item.ArtistMusicBrainzId = response.Resource.ArtistId; - item.AlbumMusicBrainzId = response.Resource.Id; - } - catch (HttpException e) - { - _logger.Warn(e, "Unable to communicate with ReadarrAPI"); - } - catch (Exception e) - { - _logger.Error(e); - } - } - - protected override void Test(List failures) - { - failures.AddIfNotNull(TestConnection()); - } - - private ValidationFailure TestConnection() - { - try - { - using (var api = GetApi()) - { - var profile = _spotifyProxy.GetPrivateProfile(this, api); - _logger.Debug($"Connected to spotify profile {profile.DisplayName} [{profile.Id}]"); - return null; - } - } - catch (SpotifyAuthorizationException ex) - { - _logger.Warn(ex, "Spotify Authentication Error"); - return new ValidationFailure(string.Empty, $"Spotify authentication error: {ex.Message}"); - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to connect to Spotify"); - - return new ValidationFailure(string.Empty, "Unable to connect to import list, check the log for more details"); - } - } - - public override object RequestAction(string action, IDictionary query) - { - if (action == "startOAuth") - { - var request = new HttpRequestBuilder(Settings.OAuthUrl) - .AddQueryParam("client_id", Settings.ClientId) - .AddQueryParam("response_type", "code") - .AddQueryParam("redirect_uri", Settings.RedirectUri) - .AddQueryParam("scope", Settings.Scope) - .AddQueryParam("state", query["callbackUrl"]) - .AddQueryParam("show_dialog", true) - .Build(); - - return new - { - OauthUrl = request.Url.ToString() - }; - } - else if (action == "getOAuthToken") - { - return new - { - accessToken = query["access_token"], - expires = DateTime.UtcNow.AddSeconds(int.Parse(query["expires_in"])), - refreshToken = query["refresh_token"], - }; - } - - return new { }; - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListItemInfo.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListItemInfo.cs deleted file mode 100644 index a9ccc0c4e..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListItemInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.ImportLists.Spotify -{ - public class SpotifyImportListItemInfo : ImportListItemInfo - { - public string ArtistSpotifyId { get; set; } - public string AlbumSpotifyId { get; set; } - - public override string ToString() - { - return string.Format("[{0}] {1}", ArtistSpotifyId, AlbumSpotifyId); - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyMap.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyMap.cs deleted file mode 100644 index 34170b98b..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyMap.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.ImportLists.Spotify -{ - public class SpotifyMap - { - public string SpotifyId { get; set; } - public string MusicbrainzId { get; set; } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs deleted file mode 100644 index 54bc232f0..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Validation; -using SpotifyAPI.Web; -using SpotifyAPI.Web.Models; - -namespace NzbDrone.Core.ImportLists.Spotify -{ - public class SpotifyPlaylist : SpotifyImportListBase - { - public SpotifyPlaylist(ISpotifyProxy spotifyProxy, - IMetadataRequestBuilder requestBuilder, - IImportListStatusService importListStatusService, - IImportListRepository importListRepository, - IConfigService configService, - IParsingService parsingService, - IHttpClient httpClient, - Logger logger) - : base(spotifyProxy, requestBuilder, importListStatusService, importListRepository, configService, parsingService, httpClient, logger) - { - } - - public override string Name => "Spotify Playlists"; - - public override IList Fetch(SpotifyWebAPI api) - { - return Settings.PlaylistIds.SelectMany(x => Fetch(api, x)).ToList(); - } - - public IList Fetch(SpotifyWebAPI api, string playlistId) - { - var result = new List(); - - _logger.Trace($"Processing playlist {playlistId}"); - - var playlistTracks = _spotifyProxy.GetPlaylistTracks(this, api, playlistId, "next, items(track(name, artists(id, name), album(id, name, release_date, release_date_precision, artists(id, name))))"); - - while (true) - { - if (playlistTracks?.Items == null) - { - return result; - } - - foreach (var playlistTrack in playlistTracks.Items) - { - result.AddIfNotNull(ParsePlaylistTrack(playlistTrack)); - } - - if (!playlistTracks.HasNextPage()) - { - break; - } - - playlistTracks = _spotifyProxy.GetNextPage(this, api, playlistTracks); - } - - return result; - } - - private SpotifyImportListItemInfo ParsePlaylistTrack(PlaylistTrack playlistTrack) - { - // From spotify docs: "Note, a track object may be null. This can happen if a track is no longer available." - if (playlistTrack?.Track?.Album != null) - { - var album = playlistTrack.Track.Album; - - var albumName = album.Name; - var artistName = album.Artists?.FirstOrDefault()?.Name ?? playlistTrack.Track?.Artists?.FirstOrDefault()?.Name; - - if (albumName.IsNotNullOrWhiteSpace() && artistName.IsNotNullOrWhiteSpace()) - { - return new SpotifyImportListItemInfo - { - Artist = artistName, - Album = album.Name, - AlbumSpotifyId = album.Id, - ReleaseDate = ParseSpotifyDate(album.ReleaseDate, album.ReleaseDatePrecision) - }; - } - } - - return null; - } - - public override object RequestAction(string action, IDictionary query) - { - if (action == "getPlaylists") - { - if (Settings.AccessToken.IsNullOrWhiteSpace()) - { - return new - { - playlists = new List() - }; - } - - Settings.Validate().Filter("AccessToken").ThrowOnError(); - - using (var api = GetApi()) - { - try - { - var profile = _spotifyProxy.GetPrivateProfile(this, api); - var playlistPage = _spotifyProxy.GetUserPlaylists(this, api, profile.Id); - _logger.Trace($"Got {playlistPage.Total} playlists"); - - var playlists = new List(playlistPage.Total); - while (true) - { - if (playlistPage == null) - { - break; - } - - playlists.AddRange(playlistPage.Items); - - if (!playlistPage.HasNextPage()) - { - break; - } - - playlistPage = _spotifyProxy.GetNextPage(this, api, playlistPage); - } - - return new - { - options = new - { - user = profile.DisplayName, - playlists = playlists.OrderBy(p => p.Name) - .Select(p => new - { - id = p.Id, - name = p.Name - }) - } - }; - } - catch (Exception ex) - { - _logger.Warn(ex, "Error fetching playlists from Spotify"); - return new { }; - } - } - } - else - { - return base.RequestAction(action, query); - } - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylistSettings.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylistSettings.cs deleted file mode 100644 index ac4d87199..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylistSettings.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using FluentValidation; -using NzbDrone.Core.Annotations; - -namespace NzbDrone.Core.ImportLists.Spotify -{ - public class SpotifyPlaylistSettingsValidator : SpotifySettingsBaseValidator - { - public SpotifyPlaylistSettingsValidator() - : base() - { - RuleFor(c => c.PlaylistIds).NotEmpty(); - } - } - - public class SpotifyPlaylistSettings : SpotifySettingsBase - { - protected override AbstractValidator Validator => new SpotifyPlaylistSettingsValidator(); - - public SpotifyPlaylistSettings() - { - PlaylistIds = new string[] { }; - } - - public override string Scope => "playlist-read-private"; - - [FieldDefinition(1, Label = "Playlists", Type = FieldType.Playlist)] - public IEnumerable PlaylistIds { get; set; } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyProxy.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyProxy.cs deleted file mode 100644 index 72e2464e9..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyProxy.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using NLog; -using SpotifyAPI.Web; -using SpotifyAPI.Web.Enums; -using SpotifyAPI.Web.Models; - -namespace NzbDrone.Core.ImportLists.Spotify -{ - public interface ISpotifyProxy - { - PrivateProfile GetPrivateProfile(SpotifyImportListBase list, SpotifyWebAPI api) - where TSettings : SpotifySettingsBase, new(); - Paging GetUserPlaylists(SpotifyImportListBase list, SpotifyWebAPI api, string id) - where TSettings : SpotifySettingsBase, new(); - FollowedArtists GetFollowedArtists(SpotifyImportListBase list, SpotifyWebAPI api) - where TSettings : SpotifySettingsBase, new(); - Paging GetSavedAlbums(SpotifyImportListBase list, SpotifyWebAPI api) - where TSettings : SpotifySettingsBase, new(); - Paging GetPlaylistTracks(SpotifyImportListBase list, SpotifyWebAPI api, string id, string fields) - where TSettings : SpotifySettingsBase, new(); - Paging GetNextPage(SpotifyImportListBase list, SpotifyWebAPI api, Paging item) - where TSettings : SpotifySettingsBase, new(); - FollowedArtists GetNextPage(SpotifyImportListBase list, SpotifyWebAPI api, FollowedArtists item) - where TSettings : SpotifySettingsBase, new(); - } - - public class SpotifyProxy : ISpotifyProxy - { - private readonly Logger _logger; - - public SpotifyProxy(Logger logger) - { - _logger = logger; - } - - public PrivateProfile GetPrivateProfile(SpotifyImportListBase list, SpotifyWebAPI api) - where TSettings : SpotifySettingsBase, new() - { - return Execute(list, api, x => x.GetPrivateProfile()); - } - - public Paging GetUserPlaylists(SpotifyImportListBase list, SpotifyWebAPI api, string id) - where TSettings : SpotifySettingsBase, new() - { - return Execute(list, api, x => x.GetUserPlaylists(id)); - } - - public FollowedArtists GetFollowedArtists(SpotifyImportListBase list, SpotifyWebAPI api) - where TSettings : SpotifySettingsBase, new() - { - return Execute(list, api, x => x.GetFollowedArtists(FollowType.Artist, 50)); - } - - public Paging GetSavedAlbums(SpotifyImportListBase list, SpotifyWebAPI api) - where TSettings : SpotifySettingsBase, new() - { - return Execute(list, api, x => x.GetSavedAlbums(50)); - } - - public Paging GetPlaylistTracks(SpotifyImportListBase list, SpotifyWebAPI api, string id, string fields) - where TSettings : SpotifySettingsBase, new() - { - return Execute(list, api, x => x.GetPlaylistTracks(id, fields: fields)); - } - - public Paging GetNextPage(SpotifyImportListBase list, SpotifyWebAPI api, Paging item) - where TSettings : SpotifySettingsBase, new() - { - return Execute(list, api, (x) => x.GetNextPage(item)); - } - - public FollowedArtists GetNextPage(SpotifyImportListBase list, SpotifyWebAPI api, FollowedArtists item) - where TSettings : SpotifySettingsBase, new() - { - return Execute(list, api, (x) => x.GetNextPage(item.Artists)); - } - - public T Execute(SpotifyImportListBase list, SpotifyWebAPI api, Func method, bool allowReauth = true) - where T : BasicModel - where TSettings : SpotifySettingsBase, new() - { - T result = method(api); - if (result.HasError()) - { - // If unauthorized, refresh token and try again - if (result.Error.Status == 401) - { - if (allowReauth) - { - _logger.Debug("Spotify authorization error, refreshing token and retrying"); - list.RefreshToken(); - api.AccessToken = list.AccessToken; - return Execute(list, api, method, false); - } - else - { - throw new SpotifyAuthorizationException(result.Error.Message); - } - } - else - { - throw new SpotifyException("[{0}] {1}", result.Error.Status, result.Error.Message); - } - } - - return result; - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs deleted file mode 100644 index 2d9004328..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Parser; -using SpotifyAPI.Web; -using SpotifyAPI.Web.Models; - -namespace NzbDrone.Core.ImportLists.Spotify -{ - public class SpotifySavedAlbumsSettings : SpotifySettingsBase - { - public override string Scope => "user-library-read"; - } - - public class SpotifySavedAlbums : SpotifyImportListBase - { - public SpotifySavedAlbums(ISpotifyProxy spotifyProxy, - IMetadataRequestBuilder requestBuilder, - IImportListStatusService importListStatusService, - IImportListRepository importListRepository, - IConfigService configService, - IParsingService parsingService, - IHttpClient httpClient, - Logger logger) - : base(spotifyProxy, requestBuilder, importListStatusService, importListRepository, configService, parsingService, httpClient, logger) - { - } - - public override string Name => "Spotify Saved Albums"; - - public override IList Fetch(SpotifyWebAPI api) - { - var result = new List(); - - var savedAlbums = _spotifyProxy.GetSavedAlbums(this, api); - - _logger.Trace($"Got {savedAlbums?.Total ?? 0} saved albums"); - - while (true) - { - if (savedAlbums?.Items == null) - { - return result; - } - - foreach (var savedAlbum in savedAlbums.Items) - { - result.AddIfNotNull(ParseSavedAlbum(savedAlbum)); - } - - if (!savedAlbums.HasNextPage()) - { - break; - } - - savedAlbums = _spotifyProxy.GetNextPage(this, api, savedAlbums); - } - - return result; - } - - private SpotifyImportListItemInfo ParseSavedAlbum(SavedAlbum savedAlbum) - { - var artistName = savedAlbum?.Album?.Artists?.FirstOrDefault()?.Name; - var albumName = savedAlbum?.Album?.Name; - _logger.Trace($"Adding {artistName} - {albumName}"); - - if (artistName.IsNotNullOrWhiteSpace() && albumName.IsNotNullOrWhiteSpace()) - { - return new SpotifyImportListItemInfo - { - Artist = artistName, - Album = albumName, - AlbumSpotifyId = savedAlbum?.Album?.Id, - ReleaseDate = ParseSpotifyDate(savedAlbum?.Album?.ReleaseDate, savedAlbum?.Album?.ReleaseDatePrecision) - }; - } - - return null; - } - } -} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifySettingsBase.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifySettingsBase.cs deleted file mode 100644 index c2ea322ae..000000000 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifySettingsBase.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.ImportLists.Spotify -{ - public class SpotifySettingsBaseValidator : AbstractValidator - where TSettings : SpotifySettingsBase - { - public SpotifySettingsBaseValidator() - { - RuleFor(c => c.AccessToken).NotEmpty(); - RuleFor(c => c.RefreshToken).NotEmpty(); - RuleFor(c => c.Expires).NotEmpty(); - } - } - - public class SpotifySettingsBase : IImportListSettings - where TSettings : SpotifySettingsBase - { - protected virtual AbstractValidator Validator => new SpotifySettingsBaseValidator(); - - public SpotifySettingsBase() - { - BaseUrl = "https://api.spotify.com/v1"; - SignIn = "startOAuth"; - } - - public string BaseUrl { get; set; } - - public string OAuthUrl => "https://accounts.spotify.com/authorize"; - public string RedirectUri => "https://spotify.readarr.audio/auth"; - public string RenewUri => "https://spotify.readarr.audio/renew"; - public string ClientId => "848082790c32436d8a0405fddca0aa18"; - public virtual string Scope => ""; - - [FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] - public string AccessToken { get; set; } - - [FieldDefinition(0, Label = "Refresh Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] - public string RefreshToken { get; set; } - - [FieldDefinition(0, Label = "Expires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] - public DateTime Expires { get; set; } - - [FieldDefinition(99, Label = "Authenticate with Spotify", Type = FieldType.OAuth)] - public string SignIn { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate((TSettings)this)); - } - } -} diff --git a/src/NzbDrone.Core/IndexerSearch/AlbumSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/AlbumSearchCommand.cs index 1e0bbda86..c0a4c873e 100644 --- a/src/NzbDrone.Core/IndexerSearch/AlbumSearchCommand.cs +++ b/src/NzbDrone.Core/IndexerSearch/AlbumSearchCommand.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Core.IndexerSearch { public class AlbumSearchCommand : Command { - public List AlbumIds { get; set; } + public List BookIds { get; set; } public override bool SendUpdatesToClient => true; @@ -13,9 +13,9 @@ namespace NzbDrone.Core.IndexerSearch { } - public AlbumSearchCommand(List albumIds) + public AlbumSearchCommand(List bookIds) { - AlbumIds = albumIds; + BookIds = bookIds; } } } diff --git a/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs b/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs index 34ee40e29..cca7fb8b1 100644 --- a/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.IndexerSearch _logger = logger; } - private void SearchForMissingAlbums(List albums, bool userInvokedSearch) + private void SearchForMissingAlbums(List albums, bool userInvokedSearch) { _logger.ProgressInfo("Performing missing search for {0} albums", albums.Count); var downloadedCount = 0; @@ -58,10 +58,10 @@ namespace NzbDrone.Core.IndexerSearch public void Execute(AlbumSearchCommand message) { - foreach (var albumId in message.AlbumIds) + foreach (var bookId in message.BookIds) { var decisions = - _nzbSearchService.AlbumSearch(albumId, false, message.Trigger == CommandTrigger.Manual, false); + _nzbSearchService.AlbumSearch(bookId, false, message.Trigger == CommandTrigger.Manual, false); var processed = _processDownloadDecisions.ProcessDecisions(decisions); _logger.ProgressInfo("Album search completed. {0} reports downloaded.", processed.Grabbed.Count); @@ -70,13 +70,13 @@ namespace NzbDrone.Core.IndexerSearch public void Execute(MissingAlbumSearchCommand message) { - List albums; + List albums; - if (message.ArtistId.HasValue) + if (message.AuthorId.HasValue) { - int artistId = message.ArtistId.Value; + int authorId = message.AuthorId.Value; - var pagingSpec = new PagingSpec + var pagingSpec = new PagingSpec { Page = 1, PageSize = 100000, @@ -84,13 +84,13 @@ namespace NzbDrone.Core.IndexerSearch SortKey = "Id" }; - pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true); + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Author.Value.Monitored == true); - albums = _albumService.AlbumsWithoutFiles(pagingSpec).Records.Where(e => e.ArtistId.Equals(artistId)).ToList(); + albums = _albumService.AlbumsWithoutFiles(pagingSpec).Records.Where(e => e.AuthorId.Equals(authorId)).ToList(); } else { - var pagingSpec = new PagingSpec + var pagingSpec = new PagingSpec { Page = 1, PageSize = 100000, @@ -98,7 +98,7 @@ namespace NzbDrone.Core.IndexerSearch SortKey = "Id" }; - pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true); + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Author.Value.Monitored == true); albums = _albumService.AlbumsWithoutFiles(pagingSpec).Records.ToList(); } @@ -111,13 +111,13 @@ namespace NzbDrone.Core.IndexerSearch public void Execute(CutoffUnmetAlbumSearchCommand message) { - Expression> filterExpression; + Expression> filterExpression; filterExpression = v => v.Monitored == true && - v.Artist.Value.Monitored == true; + v.Author.Value.Monitored == true; - var pagingSpec = new PagingSpec + var pagingSpec = new PagingSpec { Page = 1, PageSize = 100000, diff --git a/src/NzbDrone.Core/IndexerSearch/ArtistSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/ArtistSearchCommand.cs index 4233c3e7a..d5c3bd9ce 100644 --- a/src/NzbDrone.Core/IndexerSearch/ArtistSearchCommand.cs +++ b/src/NzbDrone.Core/IndexerSearch/ArtistSearchCommand.cs @@ -4,7 +4,7 @@ namespace NzbDrone.Core.IndexerSearch { public class ArtistSearchCommand : Command { - public int ArtistId { get; set; } + public int AuthorId { get; set; } public override bool SendUpdatesToClient => true; } diff --git a/src/NzbDrone.Core/IndexerSearch/ArtistSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ArtistSearchService.cs index 2e3cc5ccb..e80b1cfce 100644 --- a/src/NzbDrone.Core/IndexerSearch/ArtistSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ArtistSearchService.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.IndexerSearch public void Execute(ArtistSearchCommand message) { - var decisions = _nzbSearchService.ArtistSearch(message.ArtistId, false, message.Trigger == CommandTrigger.Manual, false); + var decisions = _nzbSearchService.ArtistSearch(message.AuthorId, false, message.Trigger == CommandTrigger.Manual, false); var processed = _processDownloadDecisions.ProcessDecisions(decisions); _logger.ProgressInfo("Artist search completed. {0} reports downloaded.", processed.Grabbed.Count); diff --git a/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs index cd8156357..a74ead1f0 100644 --- a/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs +++ b/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs @@ -4,7 +4,7 @@ namespace NzbDrone.Core.IndexerSearch { public class CutoffUnmetAlbumSearchCommand : Command { - public int? ArtistId { get; set; } + public int? AuthorId { get; set; } public override bool SendUpdatesToClient => true; @@ -12,9 +12,9 @@ namespace NzbDrone.Core.IndexerSearch { } - public CutoffUnmetAlbumSearchCommand(int artistId) + public CutoffUnmetAlbumSearchCommand(int authorId) { - ArtistId = artistId; + AuthorId = authorId; } } } diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index 8dbbe7b6c..44c53fdb5 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions { public abstract class SearchCriteriaBase { - private static readonly Regex SpecialCharacter = new Regex(@"[`'’.]", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex SpecialCharacter = new Regex(@"[`'’]", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex NonWord = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -16,9 +16,8 @@ namespace NzbDrone.Core.IndexerSearch.Definitions public virtual bool UserInvokedSearch { get; set; } public virtual bool InteractiveSearch { get; set; } - public Artist Artist { get; set; } - public List Albums { get; set; } - public List Tracks { get; set; } + public Author Artist { get; set; } + public List Albums { get; set; } public string ArtistQuery => GetQueryTitle(Artist.Name); @@ -35,6 +34,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions var cleanTitle = BeginningThe.Replace(title, string.Empty); cleanTitle = cleanTitle.Replace(" & ", " "); + cleanTitle = cleanTitle.Replace(".", " "); cleanTitle = SpecialCharacter.Replace(cleanTitle, ""); cleanTitle = NonWord.Replace(cleanTitle, "+"); diff --git a/src/NzbDrone.Core/IndexerSearch/MissingAlbumSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/MissingAlbumSearchCommand.cs index 1f4ccb7b8..86d8f76fd 100644 --- a/src/NzbDrone.Core/IndexerSearch/MissingAlbumSearchCommand.cs +++ b/src/NzbDrone.Core/IndexerSearch/MissingAlbumSearchCommand.cs @@ -4,7 +4,7 @@ namespace NzbDrone.Core.IndexerSearch { public class MissingAlbumSearchCommand : Command { - public int? ArtistId { get; set; } + public int? AuthorId { get; set; } public override bool SendUpdatesToClient => true; @@ -12,9 +12,9 @@ namespace NzbDrone.Core.IndexerSearch { } - public MissingAlbumSearchCommand(int artistId) + public MissingAlbumSearchCommand(int authorId) { - ArtistId = artistId; + AuthorId = authorId; } } } diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index 21005ad36..8c3cb77ea 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -16,8 +16,8 @@ namespace NzbDrone.Core.IndexerSearch { public interface ISearchForNzb { - List AlbumSearch(int albumId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch); - List ArtistSearch(int artistId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch); + List AlbumSearch(int bookId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch); + List ArtistSearch(int authorId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch); } public class NzbSearchService : ISearchForNzb @@ -41,19 +41,19 @@ namespace NzbDrone.Core.IndexerSearch _logger = logger; } - public List AlbumSearch(int albumId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch) + public List AlbumSearch(int bookId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch) { - var album = _albumService.GetAlbum(albumId); + var album = _albumService.GetAlbum(bookId); return AlbumSearch(album, missingOnly, userInvokedSearch, interactiveSearch); } - public List ArtistSearch(int artistId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch) + public List ArtistSearch(int authorId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch) { - var artist = _artistService.GetArtist(artistId); + var artist = _artistService.GetArtist(authorId); return ArtistSearch(artist, missingOnly, userInvokedSearch, interactiveSearch); } - public List ArtistSearch(Artist artist, bool missingOnly, bool userInvokedSearch, bool interactiveSearch) + public List ArtistSearch(Author artist, bool missingOnly, bool userInvokedSearch, bool interactiveSearch) { var searchSpec = Get(artist, userInvokedSearch, interactiveSearch); var albums = _albumService.GetAlbumsByArtist(artist.Id); @@ -65,11 +65,11 @@ namespace NzbDrone.Core.IndexerSearch return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); } - public List AlbumSearch(Album album, bool missingOnly, bool userInvokedSearch, bool interactiveSearch) + public List AlbumSearch(Book album, bool missingOnly, bool userInvokedSearch, bool interactiveSearch) { - var artist = _artistService.GetArtist(album.ArtistId); + var artist = _artistService.GetArtist(album.AuthorId); - var searchSpec = Get(artist, new List { album }, userInvokedSearch, interactiveSearch); + var searchSpec = Get(artist, new List { album }, userInvokedSearch, interactiveSearch); searchSpec.AlbumTitle = album.Title; if (album.ReleaseDate.HasValue) @@ -85,7 +85,7 @@ namespace NzbDrone.Core.IndexerSearch return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); } - private TSpec Get(Artist artist, List albums, bool userInvokedSearch, bool interactiveSearch) + private TSpec Get(Author artist, List albums, bool userInvokedSearch, bool interactiveSearch) where TSpec : SearchCriteriaBase, new() { var spec = new TSpec(); @@ -98,7 +98,7 @@ namespace NzbDrone.Core.IndexerSearch return spec; } - private static TSpec Get(Artist artist, bool userInvokedSearch, bool interactiveSearch) + private static TSpec Get(Author artist, bool userInvokedSearch, bool interactiveSearch) where TSpec : SearchCriteriaBase, new() { var spec = new TSpec(); diff --git a/src/NzbDrone.Core/Indexers/Gazelle/Gazelle.cs b/src/NzbDrone.Core/Indexers/Gazelle/Gazelle.cs index 1bcd5a000..0f2f2df30 100644 --- a/src/NzbDrone.Core/Indexers/Gazelle/Gazelle.cs +++ b/src/NzbDrone.Core/Indexers/Gazelle/Gazelle.cs @@ -5,7 +5,6 @@ using NzbDrone.Common.Cache; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Parser; -using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Gazelle { @@ -46,16 +45,6 @@ namespace NzbDrone.Core.Indexers.Gazelle return new GazelleParser(Settings); } - public override IEnumerable DefaultDefinitions - { - get - { - yield return GetDefinition("Orpheus Network", GetSettings("https://orpheus.network")); - yield return GetDefinition("REDacted", GetSettings("https://redacted.ch")); - yield return GetDefinition("Not What CD", GetSettings("https://notwhat.cd")); - } - } - private IndexerDefinition GetDefinition(string name, GazelleSettings settings) { return new IndexerDefinition diff --git a/src/NzbDrone.Core/Indexers/Headphones/Headphones.cs b/src/NzbDrone.Core/Indexers/Headphones/Headphones.cs deleted file mode 100644 index eb2c1fc63..000000000 --- a/src/NzbDrone.Core/Indexers/Headphones/Headphones.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FluentValidation.Results; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.Indexers.Headphones -{ - public class Headphones : HttpIndexerBase - { - private readonly IHeadphonesCapabilitiesProvider _capabilitiesProvider; - - public override string Name => "Headphones VIP"; - - public override DownloadProtocol Protocol => DownloadProtocol.Usenet; - - public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize; - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new HeadphonesRequestGenerator(_capabilitiesProvider) - { - PageSize = PageSize, - Settings = Settings - }; - } - - public override IParseIndexerResponse GetParser() - { - return new HeadphonesRssParser - { - Settings = Settings - }; - } - - public Headphones(IHeadphonesCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) - { - _capabilitiesProvider = capabilitiesProvider; - } - - protected override void Test(List failures) - { - base.Test(failures); - - if (failures.Any()) - { - return; - } - - failures.AddIfNotNull(TestCapabilities()); - } - - protected virtual ValidationFailure TestCapabilities() - { - try - { - var capabilities = _capabilitiesProvider.GetCapabilities(Settings); - - if (capabilities.SupportedSearchParameters != null && capabilities.SupportedSearchParameters.Contains("q")) - { - return null; - } - - return new ValidationFailure(string.Empty, "Indexer does not support required search parameters"); - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to connect to indexer: " + ex.Message); - - return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details"); - } - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilities.cs b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilities.cs deleted file mode 100644 index dacf086e4..000000000 --- a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilities.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Indexers.Newznab; - -namespace NzbDrone.Core.Indexers.Headphones -{ - public class HeadphonesCapabilities - { - public int DefaultPageSize { get; set; } - public int MaxPageSize { get; set; } - public string[] SupportedSearchParameters { get; set; } - public List Categories { get; set; } - - public HeadphonesCapabilities() - { - DefaultPageSize = 100; - MaxPageSize = 100; - SupportedSearchParameters = new[] { "q" }; - Categories = new List(); - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilitiesProvider.cs b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilitiesProvider.cs deleted file mode 100644 index 5d848b9a9..000000000 --- a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilitiesProvider.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Linq; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Indexers.Newznab; - -namespace NzbDrone.Core.Indexers.Headphones -{ - public interface IHeadphonesCapabilitiesProvider - { - HeadphonesCapabilities GetCapabilities(HeadphonesSettings settings); - } - - public class HeadphonesCapabilitiesProvider : IHeadphonesCapabilitiesProvider - { - private readonly ICached _capabilitiesCache; - private readonly IHttpClient _httpClient; - private readonly Logger _logger; - - public HeadphonesCapabilitiesProvider(ICacheManager cacheManager, IHttpClient httpClient, Logger logger) - { - _capabilitiesCache = cacheManager.GetCache(GetType()); - _httpClient = httpClient; - _logger = logger; - } - - public HeadphonesCapabilities GetCapabilities(HeadphonesSettings indexerSettings) - { - var key = indexerSettings.ToJson(); - var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings), TimeSpan.FromDays(7)); - - return capabilities; - } - - private HeadphonesCapabilities FetchCapabilities(HeadphonesSettings indexerSettings) - { - var capabilities = new HeadphonesCapabilities(); - - var url = string.Format("{0}{1}?t=caps", indexerSettings.BaseUrl.TrimEnd('/'), indexerSettings.ApiPath.TrimEnd('/')); - - if (indexerSettings.ApiKey.IsNotNullOrWhiteSpace()) - { - url += "&apikey=" + indexerSettings.ApiKey; - } - - var request = new HttpRequest(url, HttpAccept.Rss); - - request.AddBasicAuthentication(indexerSettings.Username, indexerSettings.Password); - - HttpResponse response; - - try - { - response = _httpClient.Get(request); - } - catch (Exception ex) - { - _logger.Debug(ex, "Failed to get headphones api capabilities from {0}", indexerSettings.BaseUrl); - throw; - } - - try - { - capabilities = ParseCapabilities(response); - } - catch (XmlException ex) - { - _logger.Debug(ex, "Failed to parse headphones api capabilities for {0}", indexerSettings.BaseUrl); - ex.WithData(response); - throw; - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to determine headphones api capabilities for {0}, using the defaults instead till Readarr restarts", indexerSettings.BaseUrl); - } - - return capabilities; - } - - private HeadphonesCapabilities ParseCapabilities(HttpResponse response) - { - var capabilities = new HeadphonesCapabilities(); - - var xDoc = XDocument.Parse(response.Content); - - if (xDoc == null) - { - throw new XmlException("Invalid XML"); - } - - var xmlRoot = xDoc.Element("caps"); - - if (xmlRoot == null) - { - throw new XmlException("Unexpected XML"); - } - - var xmlLimits = xmlRoot.Element("limits"); - if (xmlLimits != null) - { - capabilities.DefaultPageSize = int.Parse(xmlLimits.Attribute("default").Value); - capabilities.MaxPageSize = int.Parse(xmlLimits.Attribute("max").Value); - } - - var xmlSearching = xmlRoot.Element("searching"); - if (xmlSearching != null) - { - var xmlBasicSearch = xmlSearching.Element("search"); - if (xmlBasicSearch == null || xmlBasicSearch.Attribute("available").Value != "yes") - { - capabilities.SupportedSearchParameters = null; - } - else if (xmlBasicSearch.Attribute("supportedParams") != null) - { - capabilities.SupportedSearchParameters = xmlBasicSearch.Attribute("supportedParams").Value.Split(','); - } - } - - var xmlCategories = xmlRoot.Element("categories"); - if (xmlCategories != null) - { - foreach (var xmlCategory in xmlCategories.Elements("category")) - { - var cat = new NewznabCategory - { - Id = int.Parse(xmlCategory.Attribute("id").Value), - Name = xmlCategory.Attribute("name").Value, - Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty, - Subcategories = new List() - }; - - foreach (var xmlSubcat in xmlCategory.Elements("subcat")) - { - cat.Subcategories.Add(new NewznabCategory - { - Id = int.Parse(xmlSubcat.Attribute("id").Value), - Name = xmlSubcat.Attribute("name").Value, - Description = xmlSubcat.Attribute("description") != null ? xmlSubcat.Attribute("description").Value : string.Empty - }); - } - - capabilities.Categories.Add(cat); - } - } - - return capabilities; - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesRequestGenerator.cs deleted file mode 100644 index 348c29243..000000000 --- a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesRequestGenerator.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.IndexerSearch.Definitions; - -namespace NzbDrone.Core.Indexers.Headphones -{ - public class HeadphonesRequestGenerator : IIndexerRequestGenerator - { - private readonly IHeadphonesCapabilitiesProvider _capabilitiesProvider; - public int MaxPages { get; set; } - public int PageSize { get; set; } - public HeadphonesSettings Settings { get; set; } - - public HeadphonesRequestGenerator(IHeadphonesCapabilitiesProvider capabilitiesProvider) - { - _capabilitiesProvider = capabilitiesProvider; - - MaxPages = 30; - PageSize = 100; - } - - public virtual IndexerPageableRequestChain GetRecentRequests() - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", "")); - - return pageableRequests; - } - - public virtual IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.AddTier(); - - pageableRequests.Add(GetPagedRequests(MaxPages, - Settings.Categories, - "search", - NewsnabifyTitle($"&q={searchCriteria.ArtistQuery}+{searchCriteria.AlbumQuery}"))); - - return pageableRequests; - } - - public virtual IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.AddTier(); - - pageableRequests.Add(GetPagedRequests(MaxPages, - Settings.Categories, - "search", - NewsnabifyTitle($"&q={searchCriteria.ArtistQuery}"))); - - return pageableRequests; - } - - private IEnumerable GetPagedRequests(int maxPages, IEnumerable categories, string searchType, string parameters) - { - if (categories.Empty()) - { - yield break; - } - - var categoriesQuery = string.Join(",", categories.Distinct()); - - var baseUrl = - $"{Settings.BaseUrl.TrimEnd('/')}{Settings.ApiPath.TrimEnd('/')}?t={searchType}&cat={categoriesQuery}&extended=1"; - - if (Settings.ApiKey.IsNotNullOrWhiteSpace()) - { - baseUrl += "&apikey=" + Settings.ApiKey; - } - - if (PageSize == 0) - { - var request = new IndexerRequest($"{baseUrl}{parameters}", HttpAccept.Rss); - request.HttpRequest.AddBasicAuthentication(Settings.Username, Settings.Password); - - yield return request; - } - else - { - for (var page = 0; page < maxPages; page++) - { - var request = new IndexerRequest($"{baseUrl}&offset={page * PageSize}&limit={PageSize}{parameters}", HttpAccept.Rss); - request.HttpRequest.AddBasicAuthentication(Settings.Username, Settings.Password); - - yield return request; - } - } - } - - private static string NewsnabifyTitle(string title) - { - return title.Replace("+", "%20"); - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesRssParser.cs b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesRssParser.cs deleted file mode 100644 index 9ee7cff5d..000000000 --- a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesRssParser.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Text; -using NzbDrone.Core.Indexers.Newznab; - -namespace NzbDrone.Core.Indexers.Headphones -{ - public class HeadphonesRssParser : NewznabRssParser - { - public HeadphonesSettings Settings { get; set; } - - public HeadphonesRssParser() - { - PreferredEnclosureMimeTypes = UsenetEnclosureMimeTypes; - UseEnclosureUrl = true; - } - - protected override string GetBasicAuth() - { - return Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{Settings.Username}:{Settings.Password}")); - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesSettings.cs b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesSettings.cs deleted file mode 100644 index b01465f33..000000000 --- a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesSettings.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using FluentValidation; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Indexers.Headphones -{ - public class HeadphonesSettingsValidator : AbstractValidator - { - public HeadphonesSettingsValidator() - { - RuleFor(c => c).Custom((c, context) => - { - if (c.Categories.Empty()) - { - context.AddFailure("'Categories' must be provided"); - } - }); - - RuleFor(c => c.Username).NotEmpty(); - RuleFor(c => c.Password).NotEmpty(); - } - } - - public class HeadphonesSettings : IIndexerSettings - { - private static readonly HeadphonesSettingsValidator Validator = new HeadphonesSettingsValidator(); - - public HeadphonesSettings() - { - ApiPath = "/api"; - BaseUrl = "https://indexer.codeshy.com"; - ApiKey = "964d601959918a578a670984bdee9357"; - Categories = new[] { 3000, 3010, 3020, 3030, 3040 }; - } - - public string BaseUrl { get; set; } - - public string ApiPath { get; set; } - - public string ApiKey { get; set; } - - [FieldDefinition(0, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows", Advanced = true)] - public IEnumerable Categories { get; set; } - - [FieldDefinition(1, Label = "Username")] - public string Username { get; set; } - - [FieldDefinition(2, Label = "Password", Type = FieldType.Password)] - public string Password { get; set; } - - [FieldDefinition(3, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Readarr will download from this indexer, empty is no limit", Advanced = true)] - public int? EarlyReleaseLimit { get; set; } - - public virtual NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index f24b797f3..3ad9fc0fe 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Indexers.Newznab public NewznabSettings() { ApiPath = "/api"; - Categories = new[] { 3000, 3010, 3020, 3030, 3040 }; + Categories = new[] { 7020, 8010 }; } [FieldDefinition(0, Label = "URL")] diff --git a/src/NzbDrone.Core/Indexers/Waffles/Waffles.cs b/src/NzbDrone.Core/Indexers/Waffles/Waffles.cs deleted file mode 100644 index 9f2377fb2..000000000 --- a/src/NzbDrone.Core/Indexers/Waffles/Waffles.cs +++ /dev/null @@ -1,30 +0,0 @@ -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.Indexers.Waffles -{ - public class Waffles : HttpIndexerBase - { - public override string Name => "Waffles"; - - public override DownloadProtocol Protocol => DownloadProtocol.Torrent; - public override int PageSize => 15; - - public Waffles(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) - { - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new WafflesRequestGenerator() { Settings = Settings }; - } - - public override IParseIndexerResponse GetParser() - { - return new WafflesRssParser() { ParseSizeInDescription = true, ParseSeedersInDescription = true }; - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Waffles/WafflesRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Waffles/WafflesRequestGenerator.cs deleted file mode 100644 index fa39d562b..000000000 --- a/src/NzbDrone.Core/Indexers/Waffles/WafflesRequestGenerator.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using System.Text; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.IndexerSearch.Definitions; - -namespace NzbDrone.Core.Indexers.Waffles -{ - public class WafflesRequestGenerator : IIndexerRequestGenerator - { - public WafflesSettings Settings { get; set; } - public int MaxPages { get; set; } - - public WafflesRequestGenerator() - { - MaxPages = 5; - } - - public virtual IndexerPageableRequestChain GetRecentRequests() - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(MaxPages, null)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(MaxPages, string.Format("&q=artist:{0} album:{1}", searchCriteria.ArtistQuery, searchCriteria.AlbumQuery))); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(MaxPages, string.Format("&q=artist:{0}", searchCriteria.ArtistQuery))); - - return pageableRequests; - } - - private IEnumerable GetPagedRequests(int maxPages, string query) - { - var url = new StringBuilder(); - - url.AppendFormat("{0}/browse.php?rss=1&c0=1&uid={1}&passkey={2}", Settings.BaseUrl.Trim().TrimEnd('/'), Settings.UserId, Settings.RssPasskey); - - if (query.IsNotNullOrWhiteSpace()) - { - url.AppendFormat(query); - } - - for (var page = 0; page < maxPages; page++) - { - yield return new IndexerRequest(string.Format("{0}&p={1}", url, page), HttpAccept.Rss); - } - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Waffles/WafflesRssParser.cs b/src/NzbDrone.Core/Indexers/Waffles/WafflesRssParser.cs deleted file mode 100644 index a5744d141..000000000 --- a/src/NzbDrone.Core/Indexers/Waffles/WafflesRssParser.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Indexers.Exceptions; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Indexers.Waffles -{ - public class WafflesRssParser : TorrentRssParser - { - public const string ns = "{http://purl.org/rss/1.0/}"; - public const string dc = "{http://purl.org/dc/elements/1.1/}"; - - protected override bool PreProcess(IndexerResponse indexerResponse) - { - var xdoc = LoadXmlDocument(indexerResponse); - var error = xdoc.Descendants("error").FirstOrDefault(); - - if (error == null) - { - return true; - } - - var code = Convert.ToInt32(error.Attribute("code").Value); - var errorMessage = error.Attribute("description").Value; - - if (code >= 100 && code <= 199) - { - throw new ApiKeyException("Invalid Pass key"); - } - - if (!indexerResponse.Request.Url.FullUri.Contains("passkey=") && errorMessage == "Missing parameter") - { - throw new ApiKeyException("Indexer requires an Pass key"); - } - - if (errorMessage == "Request limit reached") - { - throw new RequestLimitReachedException("API limit reached"); - } - - throw new IndexerException(indexerResponse, errorMessage); - } - - protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo) - { - var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo; - - return torrentInfo; - } - - protected override string GetInfoUrl(XElement item) - { - return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments")); - } - - protected override string GetCommentUrl(XElement item) - { - return ParseUrl(item.TryGetValue("comments")); - } - - private static readonly Regex ParseSizeRegex = new Regex(@"(?:Size: )(?\d+)<", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - protected override long GetSize(XElement item) - { - var match = ParseSizeRegex.Matches(item.Element("description").Value); - - if (match.Count != 0) - { - var value = decimal.Parse(Regex.Replace(match[0].Groups["value"].Value, "\\,", ""), CultureInfo.InvariantCulture); - return (long)value; - } - - return 0; - } - - protected override DateTime GetPublishDate(XElement item) - { - var dateString = item.TryGetValue(dc + "date"); - - if (dateString.IsNullOrWhiteSpace()) - { - throw new UnsupportedFeedException("Rss feed must have a pubDate element with a valid publish date."); - } - - return XElementExtensions.ParseDate(dateString); - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs b/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs deleted file mode 100644 index 0b01916c9..000000000 --- a/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs +++ /dev/null @@ -1,50 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Indexers.Waffles -{ - public class WafflesSettingsValidator : AbstractValidator - { - public WafflesSettingsValidator() - { - RuleFor(c => c.BaseUrl).ValidRootUrl(); - RuleFor(c => c.UserId).NotEmpty(); - RuleFor(c => c.RssPasskey).NotEmpty(); - } - } - - public class WafflesSettings : ITorrentIndexerSettings - { - private static readonly WafflesSettingsValidator Validator = new WafflesSettingsValidator(); - - public WafflesSettings() - { - BaseUrl = "https://www.waffles.ch"; - MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; - } - - [FieldDefinition(0, Label = "Website URL")] - public string BaseUrl { get; set; } - - [FieldDefinition(1, Label = "UserId")] - public string UserId { get; set; } - - [FieldDefinition(2, Label = "RSS Passkey")] - public string RssPasskey { get; set; } - - [FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] - public int MinimumSeeders { get; set; } - - [FieldDefinition(4)] - public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); - - [FieldDefinition(5, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Readarr will download from this indexer, empty is no limit", Advanced = true)] - public int? EarlyReleaseLimit { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/MediaCover/CoverAlreadyExistsSpecification.cs b/src/NzbDrone.Core/MediaCover/CoverAlreadyExistsSpecification.cs index 8054aa8ec..ce310ed5f 100644 --- a/src/NzbDrone.Core/MediaCover/CoverAlreadyExistsSpecification.cs +++ b/src/NzbDrone.Core/MediaCover/CoverAlreadyExistsSpecification.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.MediaCover return lastModifiedLocal.Value.ToUniversalTime() == serverModifiedDate.Value.ToUniversalTime(); } - return false; + return true; } } } diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index e1a89c126..1ad38670d 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.MediaCover { void ConvertToLocalUrls(int entityId, MediaCoverEntity coverEntity, IEnumerable covers); string GetCoverPath(int entityId, MediaCoverEntity coverEntity, MediaCoverTypes mediaCoverTypes, string extension, int? height = null); - void EnsureAlbumCovers(Album album); + void EnsureAlbumCovers(Book album); } public class MediaCoverService : @@ -29,6 +29,9 @@ namespace NzbDrone.Core.MediaCover IHandleAsync, IMapCoversToLocal { + private const double HTTP_RATE_LIMIT = 0; + private const string USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 10; Mi A2 Build/QKQ1.190910.002)"; + private readonly IImageResizer _resizer; private readonly IAlbumService _albumService; private readonly IHttpClient _httpClient; @@ -103,17 +106,17 @@ namespace NzbDrone.Core.MediaCover } } - private string GetArtistCoverPath(int artistId) + private string GetArtistCoverPath(int authorId) { - return Path.Combine(_coverRootFolder, artistId.ToString()); + return Path.Combine(_coverRootFolder, authorId.ToString()); } - private string GetAlbumCoverPath(int albumId) + private string GetAlbumCoverPath(int bookId) { - return Path.Combine(_coverRootFolder, "Albums", albumId.ToString()); + return Path.Combine(_coverRootFolder, "Albums", bookId.ToString()); } - private void EnsureArtistCovers(Artist artist) + private void EnsureArtistCovers(Author artist) { var toResize = new List>(); @@ -124,13 +127,11 @@ namespace NzbDrone.Core.MediaCover try { - var serverFileHeaders = _httpClient.Head(new HttpRequest(cover.Url) { AllowAutoRedirect = true }).Headers; - - alreadyExists = _coverExistsSpecification.AlreadyExists(serverFileHeaders.LastModified, serverFileHeaders.ContentLength, fileName); + alreadyExists = _coverExistsSpecification.AlreadyExists(null, null, fileName); if (!alreadyExists) { - DownloadCover(artist, cover, serverFileHeaders.LastModified ?? DateTime.Now); + DownloadCover(artist, cover, DateTime.Now); } } catch (WebException e) @@ -160,7 +161,7 @@ namespace NzbDrone.Core.MediaCover } } - public void EnsureAlbumCovers(Album album) + public void EnsureAlbumCovers(Book album) { foreach (var cover in album.Images.Where(e => e.CoverType == MediaCoverTypes.Cover)) { @@ -168,13 +169,11 @@ namespace NzbDrone.Core.MediaCover var alreadyExists = false; try { - var serverFileHeaders = _httpClient.Head(new HttpRequest(cover.Url) { AllowAutoRedirect = true }).Headers; - - alreadyExists = _coverExistsSpecification.AlreadyExists(serverFileHeaders.LastModified, serverFileHeaders.ContentLength, fileName); + alreadyExists = _coverExistsSpecification.AlreadyExists(null, null, fileName); if (!alreadyExists) { - DownloadAlbumCover(album, cover, serverFileHeaders.LastModified ?? DateTime.Now); + DownloadAlbumCover(album, cover, DateTime.Now); } } catch (WebException e) @@ -188,12 +187,12 @@ namespace NzbDrone.Core.MediaCover } } - private void DownloadCover(Artist artist, MediaCover cover, DateTime lastModified) + private void DownloadCover(Author artist, MediaCover cover, DateTime lastModified) { var fileName = GetCoverPath(artist.Id, MediaCoverEntity.Artist, cover.CoverType, cover.Extension); _logger.Info("Downloading {0} for {1} {2}", cover.CoverType, artist, cover.Url); - _httpClient.DownloadFile(cover.Url, fileName); + _httpClient.DownloadFile(cover.Url, fileName, USER_AGENT); try { @@ -205,12 +204,12 @@ namespace NzbDrone.Core.MediaCover } } - private void DownloadAlbumCover(Album album, MediaCover cover, DateTime lastModified) + private void DownloadAlbumCover(Book album, MediaCover cover, DateTime lastModified) { var fileName = GetCoverPath(album.Id, MediaCoverEntity.Album, cover.CoverType, cover.Extension, null); _logger.Info("Downloading {0} for {1} {2}", cover.CoverType, album, cover.Url); - _httpClient.DownloadFile(cover.Url, fileName); + _httpClient.DownloadFile(cover.Url, fileName, USER_AGENT); try { @@ -222,7 +221,7 @@ namespace NzbDrone.Core.MediaCover } } - private void EnsureResizedCovers(Artist artist, MediaCover cover, bool forceResize, Album album = null) + private void EnsureResizedCovers(Author artist, MediaCover cover, bool forceResize, Book album = null) { int[] heights = GetDefaultHeights(cover.CoverType); @@ -275,7 +274,7 @@ namespace NzbDrone.Core.MediaCover EnsureArtistCovers(message.Artist); var albums = _albumService.GetAlbumsByArtist(message.Artist.Id); - foreach (Album album in albums) + foreach (Book album in albums) { EnsureAlbumCovers(album); } diff --git a/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs b/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs index 65ce089a1..0fd9fa40a 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs @@ -5,15 +5,15 @@ namespace NzbDrone.Core.MediaCover { public class MediaCoversUpdatedEvent : IEvent { - public Artist Artist { get; set; } - public Album Album { get; set; } + public Author Artist { get; set; } + public Book Album { get; set; } - public MediaCoversUpdatedEvent(Artist artist) + public MediaCoversUpdatedEvent(Author artist) { Artist = artist; } - public MediaCoversUpdatedEvent(Album album) + public MediaCoversUpdatedEvent(Book album) { Album = album; } diff --git a/src/NzbDrone.Core/MediaFiles/AudioTag.cs b/src/NzbDrone.Core/MediaFiles/AudioTag.cs index 70b3d6bf3..e40843de7 100644 --- a/src/NzbDrone.Core/MediaFiles/AudioTag.cs +++ b/src/NzbDrone.Core/MediaFiles/AudioTag.cs @@ -37,16 +37,6 @@ namespace NzbDrone.Core.MediaFiles public string[] Genres { get; set; } public string ImageFile { get; set; } public long ImageSize { get; set; } - public string MusicBrainzReleaseCountry { get; set; } - public string MusicBrainzReleaseStatus { get; set; } - public string MusicBrainzReleaseType { get; set; } - public string MusicBrainzReleaseId { get; set; } - public string MusicBrainzArtistId { get; set; } - public string MusicBrainzReleaseArtistId { get; set; } - public string MusicBrainzReleaseGroupId { get; set; } - public string MusicBrainzTrackId { get; set; } - public string MusicBrainzReleaseTrackId { get; set; } - public string MusicBrainzAlbumComment { get; set; } public bool IsValid { get; private set; } public QualityModel Quality { get; set; } @@ -86,14 +76,6 @@ namespace NzbDrone.Core.MediaFiles Duration = file.Properties.Duration; Genres = tag.Genres; ImageSize = tag.Pictures.FirstOrDefault()?.Data.Count ?? 0; - MusicBrainzReleaseCountry = tag.MusicBrainzReleaseCountry; - MusicBrainzReleaseStatus = tag.MusicBrainzReleaseStatus; - MusicBrainzReleaseType = tag.MusicBrainzReleaseType; - MusicBrainzReleaseId = tag.MusicBrainzReleaseId; - MusicBrainzArtistId = tag.MusicBrainzArtistId; - MusicBrainzReleaseArtistId = tag.MusicBrainzReleaseArtistId; - MusicBrainzReleaseGroupId = tag.MusicBrainzReleaseGroupId; - MusicBrainzTrackId = tag.MusicBrainzTrackId; DateTime tempDate; @@ -104,8 +86,6 @@ namespace NzbDrone.Core.MediaFiles Media = id3tag.GetTextAsString("TMED"); Date = ReadId3Date(id3tag, "TDRC"); OriginalReleaseDate = ReadId3Date(id3tag, "TDOR"); - MusicBrainzAlbumComment = UserTextInformationFrame.Get(id3tag, "MusicBrainz Album Comment", false)?.Text.ExclusiveOrDefault(); - MusicBrainzReleaseTrackId = UserTextInformationFrame.Get(id3tag, "MusicBrainz Release Track Id", false)?.Text.ExclusiveOrDefault(); } else if (file.TagTypesOnDisk.HasFlag(TagTypes.Xiph)) { @@ -116,19 +96,6 @@ namespace NzbDrone.Core.MediaFiles Date = DateTime.TryParse(flactag.GetField("DATE").ExclusiveOrDefault(), out tempDate) ? tempDate : default(DateTime?); OriginalReleaseDate = DateTime.TryParse(flactag.GetField("ORIGINALDATE").ExclusiveOrDefault(), out tempDate) ? tempDate : default(DateTime?); Publisher = flactag.GetField("LABEL").ExclusiveOrDefault(); - MusicBrainzAlbumComment = flactag.GetField("MUSICBRAINZ_ALBUMCOMMENT").ExclusiveOrDefault(); - MusicBrainzReleaseTrackId = flactag.GetField("MUSICBRAINZ_RELEASETRACKID").ExclusiveOrDefault(); - - // If we haven't managed to read status/type, try the alternate mapping - if (MusicBrainzReleaseStatus.IsNullOrWhiteSpace()) - { - MusicBrainzReleaseStatus = flactag.GetField("RELEASESTATUS").ExclusiveOrDefault(); - } - - if (MusicBrainzReleaseType.IsNullOrWhiteSpace()) - { - MusicBrainzReleaseType = flactag.GetField("RELEASETYPE").ExclusiveOrDefault(); - } } else if (file.TagTypesOnDisk.HasFlag(TagTypes.Ape)) { @@ -137,8 +104,6 @@ namespace NzbDrone.Core.MediaFiles Date = DateTime.TryParse(apetag.GetItem("Year")?.ToString(), out tempDate) ? tempDate : default(DateTime?); OriginalReleaseDate = DateTime.TryParse(apetag.GetItem("Original Date")?.ToString(), out tempDate) ? tempDate : default(DateTime?); Publisher = apetag.GetItem("Label")?.ToString(); - MusicBrainzAlbumComment = apetag.GetItem("MUSICBRAINZ_ALBUMCOMMENT")?.ToString(); - MusicBrainzReleaseTrackId = apetag.GetItem("MUSICBRAINZ_RELEASETRACKID")?.ToString(); } else if (file.TagTypesOnDisk.HasFlag(TagTypes.Asf)) { @@ -147,8 +112,6 @@ namespace NzbDrone.Core.MediaFiles Date = DateTime.TryParse(asftag.GetDescriptorString("WM/Year"), out tempDate) ? tempDate : default(DateTime?); OriginalReleaseDate = DateTime.TryParse(asftag.GetDescriptorString("WM/OriginalReleaseTime"), out tempDate) ? tempDate : default(DateTime?); Publisher = asftag.GetDescriptorString("WM/Publisher"); - MusicBrainzAlbumComment = asftag.GetDescriptorString("MusicBrainz/Album Comment"); - MusicBrainzReleaseTrackId = asftag.GetDescriptorString("MusicBrainz/Release Track Id"); } else if (file.TagTypesOnDisk.HasFlag(TagTypes.Apple)) { @@ -156,8 +119,6 @@ namespace NzbDrone.Core.MediaFiles Media = appletag.GetDashBox("com.apple.iTunes", "MEDIA"); Date = DateTime.TryParse(appletag.DataBoxes(FixAppleId("day")).FirstOrDefault()?.Text, out tempDate) ? tempDate : default(DateTime?); OriginalReleaseDate = DateTime.TryParse(appletag.GetDashBox("com.apple.iTunes", "Original Date"), out tempDate) ? tempDate : default(DateTime?); - MusicBrainzAlbumComment = appletag.GetDashBox("com.apple.iTunes", "MusicBrainz Album Comment"); - MusicBrainzReleaseTrackId = appletag.GetDashBox("com.apple.iTunes", "MusicBrainz Release Track Id"); } OriginalYear = OriginalReleaseDate.HasValue ? (uint)OriginalReleaseDate?.Year : 0; @@ -178,7 +139,7 @@ namespace NzbDrone.Core.MediaFiles Logger.Debug("Audio Properties: " + acodec.Description + ", Bitrate: " + bitrate + ", Sample Size: " + file.Properties.BitsPerSample + ", SampleRate: " + acodec.AudioSampleRate + ", Channels: " + acodec.AudioChannels); - Quality = QualityParser.ParseQuality(file.Name, acodec.Description, bitrate, file.Properties.BitsPerSample); + Quality = QualityParser.ParseQuality(file.Name, acodec.Description); Logger.Debug($"Quality parsed: {Quality}, Source: {Quality.QualityDetectionSource}"); MediaInfo = new MediaInfoModel @@ -214,7 +175,7 @@ namespace NzbDrone.Core.MediaFiles // make sure these are initialized to avoid errors later on if (Quality == null) { - Quality = QualityParser.ParseQuality(path, null, EstimateBitrate(file, path)); + Quality = QualityParser.ParseQuality(path); Logger.Debug($"Unable to parse qulity from tag, Quality parsed from file path: {Quality}, Source: {Quality.QualityDetectionSource}"); } @@ -346,14 +307,6 @@ namespace NzbDrone.Core.MediaFiles tag.DiscCount = DiscCount; tag.Publisher = Publisher; tag.Genres = Genres; - tag.MusicBrainzReleaseCountry = MusicBrainzReleaseCountry; - tag.MusicBrainzReleaseStatus = MusicBrainzReleaseStatus; - tag.MusicBrainzReleaseType = MusicBrainzReleaseType; - tag.MusicBrainzReleaseId = MusicBrainzReleaseId; - tag.MusicBrainzArtistId = MusicBrainzArtistId; - tag.MusicBrainzReleaseArtistId = MusicBrainzReleaseArtistId; - tag.MusicBrainzReleaseGroupId = MusicBrainzReleaseGroupId; - tag.MusicBrainzTrackId = MusicBrainzTrackId; if (ImageFile.IsNotNullOrWhiteSpace()) { @@ -366,8 +319,6 @@ namespace NzbDrone.Core.MediaFiles id3tag.SetTextFrame("TMED", Media); WriteId3Date(id3tag, "TDRC", "TYER", "TDAT", Date); WriteId3Date(id3tag, "TDOR", "TORY", null, OriginalReleaseDate); - WriteId3Tag(id3tag, "MusicBrainz Album Comment", MusicBrainzAlbumComment); - WriteId3Tag(id3tag, "MusicBrainz Release Track Id", MusicBrainzReleaseTrackId); } else if (file.TagTypes.HasFlag(TagTypes.Xiph)) { @@ -389,12 +340,6 @@ namespace NzbDrone.Core.MediaFiles flactag.SetField("TOTALDISCS", DiscCount); flactag.SetField("MEDIA", Media); flactag.SetField("LABEL", Publisher); - flactag.SetField("MUSICBRAINZ_ALBUMCOMMENT", MusicBrainzAlbumComment); - flactag.SetField("MUSICBRAINZ_RELEASETRACKID", MusicBrainzReleaseTrackId); - - // Add the alternate mappings used by picard (we write both) - flactag.SetField("RELEASESTATUS", MusicBrainzReleaseStatus); - flactag.SetField("RELEASETYPE", MusicBrainzReleaseType); } else if (file.TagTypes.HasFlag(TagTypes.Ape)) { @@ -405,8 +350,6 @@ namespace NzbDrone.Core.MediaFiles apetag.SetValue("Original Year", OriginalReleaseDate.HasValue ? OriginalReleaseDate.Value.Year.ToString() : null); apetag.SetValue("Media", Media); apetag.SetValue("Label", Publisher); - apetag.SetValue("MUSICBRAINZ_ALBUMCOMMENT", MusicBrainzAlbumComment); - apetag.SetValue("MUSICBRAINZ_RELEASETRACKID", MusicBrainzReleaseTrackId); } else if (file.TagTypes.HasFlag(TagTypes.Asf)) { @@ -417,8 +360,6 @@ namespace NzbDrone.Core.MediaFiles asftag.SetDescriptorString(OriginalReleaseDate.HasValue ? OriginalReleaseDate.Value.Year.ToString() : null, "WM/OriginalReleaseYear"); asftag.SetDescriptorString(Media, "WM/Media"); asftag.SetDescriptorString(Publisher, "WM/Publisher"); - asftag.SetDescriptorString(MusicBrainzAlbumComment, "MusicBrainz/Album Comment"); - asftag.SetDescriptorString(MusicBrainzReleaseTrackId, "MusicBrainz/Release Track Id"); } else if (file.TagTypes.HasFlag(TagTypes.Apple)) { @@ -428,8 +369,6 @@ namespace NzbDrone.Core.MediaFiles appletag.SetDashBox("com.apple.iTunes", "Original Date", OriginalReleaseDate.HasValue ? OriginalReleaseDate.Value.ToString("yyyy-MM-dd") : null); appletag.SetDashBox("com.apple.iTunes", "Original Year", OriginalReleaseDate.HasValue ? OriginalReleaseDate.Value.Year.ToString() : null); appletag.SetDashBox("com.apple.iTunes", "MEDIA", Media); - appletag.SetDashBox("com.apple.iTunes", "MusicBrainz Album Comment", MusicBrainzAlbumComment); - appletag.SetDashBox("com.apple.iTunes", "MusicBrainz Release Track Id", MusicBrainzReleaseTrackId); } file.Save(); @@ -585,25 +524,14 @@ namespace NzbDrone.Core.MediaFiles { AlbumTitle = tag.Album, ArtistTitle = artist, - ArtistMBId = tag.MusicBrainzReleaseArtistId, - AlbumMBId = tag.MusicBrainzReleaseGroupId, - ReleaseMBId = tag.MusicBrainzReleaseId, - - // SIC: the recording ID is stored in this field. - // See https://picard.musicbrainz.org/docs/mappings/ - RecordingMBId = tag.MusicBrainzTrackId, - TrackMBId = tag.MusicBrainzReleaseTrackId, DiscNumber = (int)tag.Disc, DiscCount = (int)tag.DiscCount, Year = tag.Year, Label = tag.Publisher, TrackNumbers = new[] { (int)tag.Track }, - ArtistTitleInfo = artistTitleInfo, Title = tag.Title, CleanTitle = tag.Title?.CleanTrackTitle(), - Country = IsoCountries.Find(tag.MusicBrainzReleaseCountry), Duration = tag.Duration, - Disambiguation = tag.MusicBrainzAlbumComment, Quality = tag.Quality, MediaInfo = tag.MediaInfo }; diff --git a/src/NzbDrone.Core/MediaFiles/AudioTagService.cs b/src/NzbDrone.Core/MediaFiles/AudioTagService.cs index 67d517251..8df9730b1 100644 --- a/src/NzbDrone.Core/MediaFiles/AudioTagService.cs +++ b/src/NzbDrone.Core/MediaFiles/AudioTagService.cs @@ -22,14 +22,10 @@ namespace NzbDrone.Core.MediaFiles public interface IAudioTagService { ParsedTrackInfo ReadTags(string file); - void WriteTags(TrackFile trackfile, bool newDownload, bool force = false); - void SyncTags(List tracks); - void RemoveMusicBrainzTags(IEnumerable album); - void RemoveMusicBrainzTags(IEnumerable albumRelease); - void RemoveMusicBrainzTags(IEnumerable tracks); - void RemoveMusicBrainzTags(TrackFile trackfile); - List GetRetagPreviewsByArtist(int artistId); - List GetRetagPreviewsByAlbum(int artistId); + void WriteTags(BookFile trackfile, bool newDownload, bool force = false); + void SyncTags(List tracks); + List GetRetagPreviewsByArtist(int authorId); + List GetRetagPreviewsByAlbum(int authorId); } public class AudioTagService : IAudioTagService, @@ -74,67 +70,12 @@ namespace NzbDrone.Core.MediaFiles return new AudioTag(path); } - public AudioTag GetTrackMetadata(TrackFile trackfile) + public AudioTag GetTrackMetadata(BookFile trackfile) { - var track = trackfile.Tracks.Value[0]; - var release = track.AlbumRelease.Value; - var album = release.Album.Value; - var albumartist = album.Artist.Value; - var artist = track.ArtistMetadata.Value; - - var cover = album.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover); - string imageFile = null; - long imageSize = 0; - if (cover != null) - { - imageFile = _mediaCoverService.GetCoverPath(album.Id, MediaCoverEntity.Album, cover.CoverType, cover.Extension, null); - _logger.Trace($"Embedding: {imageFile}"); - var fileInfo = _diskProvider.GetFileInfo(imageFile); - if (fileInfo.Exists) - { - imageSize = fileInfo.Length; - } - else - { - imageFile = null; - } - } - - return new AudioTag - { - Title = track.Title, - Performers = new[] { artist.Name }, - AlbumArtists = new[] { albumartist.Name }, - Track = (uint)track.AbsoluteTrackNumber, - TrackCount = (uint)release.Tracks.Value.Count(x => x.MediumNumber == track.MediumNumber), - Album = album.Title, - Disc = (uint)track.MediumNumber, - DiscCount = (uint)release.Media.Count, - - // We may have omitted media so index in the list isn't the same as medium number - Media = release.Media.SingleOrDefault(x => x.Number == track.MediumNumber).Format, - Date = release.ReleaseDate, - Year = (uint)album.ReleaseDate?.Year, - OriginalReleaseDate = album.ReleaseDate, - OriginalYear = (uint)album.ReleaseDate?.Year, - Publisher = release.Label.FirstOrDefault(), - Genres = album.Genres.Any() ? album.Genres.ToArray() : artist.Genres.ToArray(), - ImageFile = imageFile, - ImageSize = imageSize, - MusicBrainzReleaseCountry = IsoCountries.Find(release.Country.FirstOrDefault())?.TwoLetterCode, - MusicBrainzReleaseStatus = release.Status.ToLower(), - MusicBrainzReleaseType = album.AlbumType.ToLower(), - MusicBrainzReleaseId = release.ForeignReleaseId, - MusicBrainzArtistId = artist.ForeignArtistId, - MusicBrainzReleaseArtistId = albumartist.ForeignArtistId, - MusicBrainzReleaseGroupId = album.ForeignAlbumId, - MusicBrainzTrackId = track.ForeignRecordingId, - MusicBrainzReleaseTrackId = track.ForeignTrackId, - MusicBrainzAlbumComment = album.Disambiguation, - }; + return new AudioTag(); } - private void UpdateTrackfileSizeAndModified(TrackFile trackfile, string path) + private void UpdateTrackfileSizeAndModified(BookFile trackfile, string path) { // update the saved file size so that the importer doesn't get confused on the next scan var fileInfo = _diskProvider.GetFileInfo(path); @@ -174,26 +115,7 @@ namespace NzbDrone.Core.MediaFiles } } - public void RemoveMusicBrainzTags(string path) - { - var tags = new AudioTag(path); - - tags.MusicBrainzReleaseCountry = null; - tags.MusicBrainzReleaseStatus = null; - tags.MusicBrainzReleaseType = null; - tags.MusicBrainzReleaseId = null; - tags.MusicBrainzArtistId = null; - tags.MusicBrainzReleaseArtistId = null; - tags.MusicBrainzReleaseGroupId = null; - tags.MusicBrainzTrackId = null; - tags.MusicBrainzAlbumComment = null; - tags.MusicBrainzReleaseTrackId = null; - - _rootFolderWatchingService.ReportFileSystemChangeBeginning(path); - tags.Write(path); - } - - public void WriteTags(TrackFile trackfile, bool newDownload, bool force = false) + public void WriteTags(BookFile trackfile, bool newDownload, bool force = false) { if (!force) { @@ -204,12 +126,6 @@ namespace NzbDrone.Core.MediaFiles } } - if (trackfile.Tracks.Value.Count > 1) - { - _logger.Debug($"File {trackfile} is linked to multiple tracks. Not writing tags."); - return; - } - var newTags = GetTrackMetadata(trackfile); var path = trackfile.Path; @@ -232,7 +148,7 @@ namespace NzbDrone.Core.MediaFiles _eventAggregator.PublishEvent(new TrackFileRetaggedEvent(trackfile.Artist.Value, trackfile, diff, _configService.ScrubAudioTags)); } - public void SyncTags(List tracks) + public void SyncTags(List books) { if (_configService.WriteAudioTags != WriteAudioTagsType.Sync) { @@ -240,113 +156,45 @@ namespace NzbDrone.Core.MediaFiles } // get the tracks to update - var trackFiles = _mediaFileService.Get(tracks.Where(x => x.TrackFileId > 0).Select(x => x.TrackFileId)); - - _logger.Debug($"Syncing audio tags for {trackFiles.Count} files"); - - foreach (var file in trackFiles) - { - // populate tracks (which should also have release/album/artist set) because - // not all of the updates will have been committed to the database yet - file.Tracks = tracks.Where(x => x.TrackFileId == file.Id).ToList(); - WriteTags(file, false); - } - } - - public void RemoveMusicBrainzTags(IEnumerable albums) - { - if (_configService.WriteAudioTags < WriteAudioTagsType.AllFiles) + foreach (var book in books) { - return; - } + var trackFiles = book.BookFiles.Value; - foreach (var album in albums) - { - var files = _mediaFileService.GetFilesByAlbum(album.Id); - foreach (var file in files) - { - RemoveMusicBrainzTags(file); - } - } - } + _logger.Debug($"Syncing audio tags for {trackFiles.Count} files"); - public void RemoveMusicBrainzTags(IEnumerable releases) - { - if (_configService.WriteAudioTags < WriteAudioTagsType.AllFiles) - { - return; - } - - foreach (var release in releases) - { - var files = _mediaFileService.GetFilesByRelease(release.Id); - foreach (var file in files) + foreach (var file in trackFiles) { - RemoveMusicBrainzTags(file); + // populate tracks (which should also have release/album/artist set) because + // not all of the updates will have been committed to the database yet + file.Album = book; + WriteTags(file, false); } } } - public void RemoveMusicBrainzTags(IEnumerable tracks) + public List GetRetagPreviewsByArtist(int authorId) { - if (_configService.WriteAudioTags < WriteAudioTagsType.AllFiles) - { - return; - } - - var files = _mediaFileService.Get(tracks.Where(x => x.TrackFileId > 0).Select(x => x.TrackFileId)); - foreach (var file in files) - { - RemoveMusicBrainzTags(file); - } - } - - public void RemoveMusicBrainzTags(TrackFile trackfile) - { - if (_configService.WriteAudioTags < WriteAudioTagsType.AllFiles) - { - return; - } - - var path = trackfile.Path; - _logger.Debug($"Removing MusicBrainz tags for {path}"); - - RemoveMusicBrainzTags(path); - - UpdateTrackfileSizeAndModified(trackfile, path); - } - - public List GetRetagPreviewsByArtist(int artistId) - { - var files = _mediaFileService.GetFilesByArtist(artistId); + var files = _mediaFileService.GetFilesByArtist(authorId); return GetPreviews(files).ToList(); } - public List GetRetagPreviewsByAlbum(int albumId) + public List GetRetagPreviewsByAlbum(int bookId) { - var files = _mediaFileService.GetFilesByAlbum(albumId); + var files = _mediaFileService.GetFilesByAlbum(bookId); return GetPreviews(files).ToList(); } - private IEnumerable GetPreviews(List files) + private IEnumerable GetPreviews(List files) { - foreach (var f in files.OrderBy(x => x.Album.Value.Title) - .ThenBy(x => x.Tracks.Value.First().MediumNumber) - .ThenBy(x => x.Tracks.Value.First().AbsoluteTrackNumber)) + foreach (var f in files.OrderBy(x => x.Album.Value.Title)) { var file = f; - if (!f.Tracks.Value.Any()) - { - _logger.Warn($"File {f} is not linked to any tracks"); - continue; - } - - if (f.Tracks.Value.Count > 1) + if (f.Album.Value == null) { - _logger.Debug($"File {f} is linked to multiple tracks. Not writing tags."); + _logger.Warn($"File {f} is not linked to any books"); continue; } @@ -358,9 +206,8 @@ namespace NzbDrone.Core.MediaFiles { yield return new RetagTrackFilePreview { - ArtistId = file.Artist.Value.Id, - AlbumId = file.Album.Value.Id, - TrackNumbers = file.Tracks.Value.Select(e => e.AbsoluteTrackNumber).ToList(), + AuthorId = file.Artist.Value.Id, + BookId = file.Album.Value.Id, TrackFileId = file.Id, Path = file.Path, Changes = diff @@ -371,7 +218,7 @@ namespace NzbDrone.Core.MediaFiles public void Execute(RetagFilesCommand message) { - var artist = _artistService.GetArtist(message.ArtistId); + var artist = _artistService.GetArtist(message.AuthorId); var trackFiles = _mediaFileService.Get(message.Files); _logger.ProgressInfo("Re-tagging {0} files for {1}", trackFiles.Count, artist.Name); @@ -386,7 +233,7 @@ namespace NzbDrone.Core.MediaFiles public void Execute(RetagArtistCommand message) { _logger.Debug("Re-tagging all files for selected artists"); - var artistToRename = _artistService.GetArtists(message.ArtistIds); + var artistToRename = _artistService.GetArtists(message.AuthorIds); foreach (var artist in artistToRename) { diff --git a/src/NzbDrone.Core/MediaFiles/AzwTag/Azw3File.cs b/src/NzbDrone.Core/MediaFiles/AzwTag/Azw3File.cs new file mode 100644 index 000000000..816daa73e --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/AzwTag/Azw3File.cs @@ -0,0 +1,26 @@ +namespace NzbDrone.Core.MediaFiles.Azw +{ + public class Azw3File : AzwFile + { + public Azw3File(string path) + : base(path) + { + MobiHeader = new MobiHeader(GetSectionData(0)); + } + + public string Title => MobiHeader.Title; + public string Author => MobiHeader.ExtMeta.StringOrNull(100); + public string Isbn => MobiHeader.ExtMeta.StringOrNull(104); + public string Asin => MobiHeader.ExtMeta.StringOrNull(113); + public string PublishDate => MobiHeader.ExtMeta.StringOrNull(106); + public string Publisher => MobiHeader.ExtMeta.StringOrNull(101); + public string Imprint => MobiHeader.ExtMeta.StringOrNull(102); + public string Description => MobiHeader.ExtMeta.StringOrNull(103); + public string Source => MobiHeader.ExtMeta.StringOrNull(112); + public string Language => MobiHeader.ExtMeta.StringOrNull(524); + public uint Version => MobiHeader.Version; + public uint MobiType => MobiHeader.MobiType; + + private MobiHeader MobiHeader { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/AzwTag/Headers.cs b/src/NzbDrone.Core/MediaFiles/AzwTag/Headers.cs new file mode 100644 index 000000000..c5ac66801 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/AzwTag/Headers.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Azw +{ + public class ExtMeta + { + public Dictionary IdValue; + public Dictionary IdString; + public Dictionary IdHex; + + public ExtMeta(byte[] ext, Encoding encoding) + { + IdValue = new Dictionary(); + IdString = new Dictionary(); + IdHex = new Dictionary(); + + var num_items = Util.GetUInt32(ext, 8); + uint pos = 12; + for (var i = 0; i < num_items; i++) + { + var id = Util.GetUInt32(ext, pos); + var size = Util.GetUInt32(ext, pos + 4); + if (IdMapping.Id_map_strings.ContainsKey(id)) + { + var a = encoding.GetString(Util.SubArray(ext, pos + 8, size - 8)); + + if (IdString.ContainsKey(id)) + { + if (id == 100 || id == 517) + { + IdString[id] += "&" + a; + } + else + { + Console.WriteLine(string.Format("Meta id duplicate:{0}\nPervious:{1} \nLatter:{2}", IdMapping.Id_map_strings[id], IdString[id], a)); + } + } + else + { + IdString.Add(id, a); + } + } + else if (IdMapping.Id_map_values.ContainsKey(id)) + { + ulong a = 0; + switch (size) + { + case 9: a = Util.GetUInt8(ext, pos + 8); break; + case 10: a = Util.GetUInt16(ext, pos + 8); break; + case 12: a = Util.GetUInt32(ext, pos + 8); break; + case 16: a = Util.GetUInt64(ext, pos + 8); break; + default: Console.WriteLine("unexpected size:" + size); break; + } + + if (IdValue.ContainsKey(id)) + { + Console.WriteLine(string.Format("Meta id duplicate:{0}\nPervious:{1} \nLatter:{2}", IdMapping.Id_map_values[id], IdValue[id], a)); + } + else + { + IdValue.Add(id, a); + } + } + else if (IdMapping.Id_map_hex.ContainsKey(id)) + { + var a = Util.ToHexString(ext, pos + 8, size - 8); + + if (IdHex.ContainsKey(id)) + { + Console.WriteLine(string.Format("Meta id duplicate:{0}\nPervious:{1} \nLatter:{2}", IdMapping.Id_map_hex[id], IdHex[id], a)); + } + else + { + IdHex.Add(id, a); + } + } + else + { + // Unknown id + } + + pos += size; + } + } + + public string StringOrNull(uint key) + { + return IdString.TryGetValue(key, out var value) ? value : null; + } + } + + public class MobiHeader : Section + { + private readonly uint _length; + private readonly uint _codepage; + private readonly uint _exth_flag; + + public MobiHeader(byte[] header) + : base("Mobi Header", header) + { + var mobi = Encoding.ASCII.GetString(header, 16, 4); + if (mobi != "MOBI") + { + throw new AzwTagException("Invalid mobi header"); + } + + Version = Util.GetUInt32(header, 36); + MobiType = Util.GetUInt32(header, 24); + + _codepage = Util.GetUInt32(header, 28); + + var encoding = _codepage == 65001 ? Encoding.UTF8 : CodePagesEncodingProvider.Instance.GetEncoding((int)_codepage); + Title = encoding.GetString(header, (int)Util.GetUInt32(header, 0x54), (int)Util.GetUInt32(header, 0x58)); + + _exth_flag = Util.GetUInt32(header, 0x80); + _length = Util.GetUInt32(header, 20); + if ((_exth_flag & 0x40) > 0) + { + var exth = Util.SubArray(header, _length + 16, Util.GetUInt32(header, _length + 20)); + ExtMeta = new ExtMeta(exth, encoding); + } + else + { + throw new AzwTagException("No EXTH header. Readarr cannot process this file."); + } + } + + public string Title { get; private set; } + public uint Version { get; private set; } + public uint MobiType { get; private set; } + public ExtMeta ExtMeta { get; private set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/AzwTag/ProcessSection.cs b/src/NzbDrone.Core/MediaFiles/AzwTag/ProcessSection.cs new file mode 100644 index 000000000..7bdb1a7ee --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/AzwTag/ProcessSection.cs @@ -0,0 +1,47 @@ +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Azw +{ + public class Section + { + public string Type; + public byte[] Raw; + public string Comment = ""; + + public Section(byte[] raw) + { + Raw = raw; + if (raw.Length < 4) + { + Type = "Empty Section"; + return; + } + + Type = Encoding.ASCII.GetString(raw, 0, 4); + + switch (Type) + { + case "??\r\n": Type = "End Of File"; break; + case "?6?\t": Type = "Place Holder"; break; + case "\0\0\0\0": Type = "Empty Section0"; break; + } + } + + public Section(Section s) + { + Type = s.Type; + Raw = s.Raw; + } + + public Section(string type, byte[] raw) + { + Type = type; + Raw = raw; + } + + public virtual int GetSize() + { + return Raw.Length; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/AzwTag/Structs.cs b/src/NzbDrone.Core/MediaFiles/AzwTag/Structs.cs new file mode 100644 index 000000000..84754b349 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/AzwTag/Structs.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Azw +{ + public struct SectionInfo + { + public ulong Start_addr; + public ulong End_addr; + + public ulong Length => End_addr - Start_addr; + } + + public class AzwFile + { + public byte[] Raw_data; + public ushort Section_count; + public SectionInfo[] Section_info; + public string Ident; + + protected AzwFile(string path) + { + Raw_data = File.ReadAllBytes(path); + GetSectionInfo(); + + if (Ident != "BOOKMOBI" || Section_count == 0) + { + throw new AzwTagException("Invalid mobi header"); + } + } + + protected void GetSectionInfo() + { + Ident = Encoding.ASCII.GetString(Raw_data, 0x3c, 8); + Section_count = Util.GetUInt16(Raw_data, 76); + Section_info = new SectionInfo[Section_count]; + + Section_info[0].Start_addr = Util.GetUInt32(Raw_data, 78); + for (uint i = 1; i < Section_count; i++) + { + Section_info[i].Start_addr = Util.GetUInt32(Raw_data, 78 + (i * 8)); + Section_info[i - 1].End_addr = Section_info[i].Start_addr; + } + + Section_info[Section_count - 1].End_addr = (ulong)Raw_data.Length; + } + + protected byte[] GetSectionData(uint i) + { + return Util.SubArray(Raw_data, Section_info[i].Start_addr, Section_info[i].Length); + } + } + + public class IdMapping + { + public static Dictionary Id_map_strings = new Dictionary + { + { 1, "Drm Server Id (1)" }, + { 2, "Drm Commerce Id (2)" }, + { 3, "Drm Ebookbase Book Id(3)" }, + { 100, "Creator_(100)" }, + { 101, "Publisher_(101)" }, + { 102, "Imprint_(102)" }, + { 103, "Description_(103)" }, + { 104, "ISBN_(104)" }, + { 105, "Subject_(105)" }, + { 106, "Published_(106)" }, + { 107, "Review_(107)" }, + { 108, "Contributor_(108)" }, + { 109, "Rights_(109)" }, + { 110, "SubjectCode_(110)" }, + { 111, "Type_(111)" }, + { 112, "Source_(112)" }, + { 113, "ASIN_(113)" }, + { 114, "versionNumber_(114)" }, + { 117, "Adult_(117)" }, + { 118, "Price_(118)" }, + { 119, "Currency_(119)" }, + { 122, "fixed-layout_(122)" }, + { 123, "book-type_(123)" }, + { 124, "orientation-lock_(124)" }, + { 126, "original-resolution_(126)" }, + { 127, "zero-gutter_(127)" }, + { 128, "zero-margin_(128)" }, + { 129, "K8_Masthead/Cover_Image_(129)" }, + { 132, "RegionMagnification_(132)" }, + { 200, "DictShortName_(200)" }, + { 208, "Watermark_(208)" }, + { 501, "cdeType_(501)" }, + { 502, "last_update_time_(502)" }, + { 503, "Updated_Title_(503)" }, + { 504, "ASIN_(504)" }, + { 508, "Title_Katagana_(508)" }, + { 517, "Creator_Katagana_(517)" }, + { 522, "Publisher_Katagana_(522)" }, + { 524, "Language_(524)" }, + { 525, "primary-writing-mode_(525)" }, + { 526, "Unknown_(526)" }, + { 527, "page-progression-direction_(527)" }, + { 528, "override-kindle_fonts_(528)" }, + { 529, "Unknown_(529)" }, + { 534, "Input_Source_Type_(534)" }, + { 535, "Kindlegen_BuildRev_Number_(535)" }, + { 536, "Container_Info_(536)" }, // CONT_Header is 0, Ends with CONTAINER_BOUNDARY (or Asset_Type?) + { 538, "Container_Resolution_(538)" }, + { 539, "Container_Mimetype_(539)" }, + { 542, "Unknown_but_changes_with_filename_only_(542)" }, + { 543, "Container_id_(543)" }, // FONT_CONTAINER, BW_CONTAINER, HD_CONTAINER + { 544, "Unknown_(544)" } + }; + + public static Dictionary Id_map_values = new Dictionary() + { + { 115, "sample_(115)" }, + { 116, "StartOffset_(116)" }, + { 121, "K8(121)_Boundary_Section_(121)" }, + { 125, "K8_Count_of_Resources_Fonts_Images_(125)" }, + { 131, "K8_Unidentified_Count_(131)" }, + { 201, "CoverOffset_(201)" }, + { 202, "ThumbOffset_(202)" }, + { 203, "Fake_Cover_(203)" }, + { 204, "Creator_Software_(204)" }, + { 205, "Creator_Major_Version_(205)" }, + { 206, "Creator_Minor_Version_(206)" }, + { 207, "Creator_Build_Number_(207)" }, + { 401, "Clipping_Limit_(401)" }, + { 402, "Publisher_Limit_(402)" }, + { 404, "Text_to_Speech_Disabled_(404)" }, + { 406, "Rental_Indicator_(406)" } + }; + + public static Dictionary Id_map_hex = new Dictionary() + { + { 208, "Watermark(208 in hex)" }, + { 209, "Tamper_Proof_Keys_(209_in_hex)" }, + { 300, "Font_Signature_(300_in_hex)" } + }; + } +} diff --git a/src/NzbDrone.Core/MediaFiles/AzwTag/Utils.cs b/src/NzbDrone.Core/MediaFiles/AzwTag/Utils.cs new file mode 100644 index 000000000..ea5e98132 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/AzwTag/Utils.cs @@ -0,0 +1,88 @@ +using System; + +namespace NzbDrone.Core.MediaFiles.Azw +{ + public class Util + { + public static byte[] SubArray(byte[] src, ulong start, ulong length) + { + var r = new byte[length]; + for (ulong i = 0; i < length; i++) + { + r[i] = src[start + i]; + } + + return r; + } + + public static byte[] SubArray(byte[] src, int start, int length) + { + var r = new byte[length]; + + for (var i = 0; i < length; i++) + { + r[i] = src[start + i]; + } + + return r; + } + + public static string ToHexString(byte[] src, uint start, uint length) + { + //https://stackoverflow.com/a/14333437/48700 + var c = new char[length * 2]; + int b; + for (var i = 0; i < length; i++) + { + b = src[start + i] >> 4; + c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7)); + b = src[start + i] & 0xF; + c[(i * 2) + 1] = (char)(55 + b + (((b - 10) >> 31) & -7)); + } + + return new string(c); + } + + public static ulong GetUInt64(byte[] src, ulong start) + { + var t = SubArray(src, start, 8); + Array.Reverse(t); + return BitConverter.ToUInt64(t, 0); + } + + //big edian handle: + public static uint GetUInt32(byte[] src, ulong start) + { + var t = SubArray(src, start, 4); + Array.Reverse(t); + return BitConverter.ToUInt32(t, 0); + } + + public static ushort GetUInt16(byte[] src, ulong start) + { + var t = SubArray(src, start, 2); + Array.Reverse(t); + return BitConverter.ToUInt16(t, 0); + } + + public static byte GetUInt8(byte[] src, ulong start) + { + return src[start]; + } + } + + [Serializable] + public class AzwTagException : Exception + { + public AzwTagException(string message) + : base(message) + { + } + + protected AzwTagException(System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackFile.cs b/src/NzbDrone.Core/MediaFiles/BookFile.cs similarity index 82% rename from src/NzbDrone.Core/MediaFiles/TrackFile.cs rename to src/NzbDrone.Core/MediaFiles/BookFile.cs index 182dd5a3e..0382c93ce 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFile.cs +++ b/src/NzbDrone.Core/MediaFiles/BookFile.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Qualities; namespace NzbDrone.Core.MediaFiles { - public class TrackFile : ModelBase + public class BookFile : ModelBase { // these are model properties public string Path { get; set; } @@ -19,12 +19,12 @@ namespace NzbDrone.Core.MediaFiles public string ReleaseGroup { get; set; } public QualityModel Quality { get; set; } public MediaInfoModel MediaInfo { get; set; } - public int AlbumId { get; set; } + public int BookId { get; set; } + public int CalibreId { get; set; } // These are queried from the database - public LazyLoaded> Tracks { get; set; } - public LazyLoaded Artist { get; set; } - public LazyLoaded Album { get; set; } + public LazyLoaded Artist { get; set; } + public LazyLoaded Album { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs index 1e027f570..b35bcb112 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs @@ -5,14 +5,14 @@ namespace NzbDrone.Core.MediaFiles.Commands { public class RenameArtistCommand : Command { - public List ArtistIds { get; set; } + public List AuthorIds { get; set; } public override bool SendUpdatesToClient => true; public override bool RequiresDiskAccess => true; public RenameArtistCommand() { - ArtistIds = new List(); + AuthorIds = new List(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs index e7464a2ad..5720728dc 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Core.MediaFiles.Commands { public class RenameFilesCommand : Command { - public int ArtistId { get; set; } + public int AuthorId { get; set; } public List Files { get; set; } public override bool SendUpdatesToClient => true; @@ -15,9 +15,9 @@ namespace NzbDrone.Core.MediaFiles.Commands { } - public RenameFilesCommand(int artistId, List files) + public RenameFilesCommand(int authorId, List files) { - ArtistId = artistId; + AuthorId = authorId; Files = files; } } diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RescanFoldersCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RescanFoldersCommand.cs index e63a70135..dfbba7b44 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RescanFoldersCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RescanFoldersCommand.cs @@ -12,18 +12,18 @@ namespace NzbDrone.Core.MediaFiles.Commands AddNewArtists = true; } - public RescanFoldersCommand(List folders, FilterFilesType filter, bool addNewArtists, List artistIds) + public RescanFoldersCommand(List folders, FilterFilesType filter, bool addNewArtists, List authorIds) { Folders = folders; Filter = filter; AddNewArtists = addNewArtists; - ArtistIds = artistIds; + AuthorIds = authorIds; } public List Folders { get; set; } public FilterFilesType Filter { get; set; } public bool AddNewArtists { get; set; } - public List ArtistIds { get; set; } + public List AuthorIds { get; set; } public override bool SendUpdatesToClient => true; public override bool RequiresDiskAccess => true; diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RetagArtistCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RetagArtistCommand.cs index 6ae6514f1..891fe85bb 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RetagArtistCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RetagArtistCommand.cs @@ -5,14 +5,14 @@ namespace NzbDrone.Core.MediaFiles.Commands { public class RetagArtistCommand : Command { - public List ArtistIds { get; set; } + public List AuthorIds { get; set; } public override bool SendUpdatesToClient => true; public override bool RequiresDiskAccess => true; public RetagArtistCommand() { - ArtistIds = new List(); + AuthorIds = new List(); } } } diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RetagFilesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RetagFilesCommand.cs index dcee0d979..8f0bd745e 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RetagFilesCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RetagFilesCommand.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Core.MediaFiles.Commands { public class RetagFilesCommand : Command { - public int ArtistId { get; set; } + public int AuthorId { get; set; } public List Files { get; set; } public override bool SendUpdatesToClient => true; @@ -15,9 +15,9 @@ namespace NzbDrone.Core.MediaFiles.Commands { } - public RetagFilesCommand(int artistId, List files) + public RetagFilesCommand(int authorId, List files) { - ArtistId = artistId; + AuthorId = authorId; Files = files; } } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 62f1f1664..ac5c7ec20 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -10,19 +10,22 @@ using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Books.Calibre; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; +using NzbDrone.Core.Parser; using NzbDrone.Core.RootFolders; namespace NzbDrone.Core.MediaFiles { public interface IDiskScanService { - void Scan(List folders = null, FilterFilesType filter = FilterFilesType.Known, bool addNewArtists = false, List artistIds = null); + void Scan(List folders = null, FilterFilesType filter = FilterFilesType.Known, bool addNewArtists = false, List authorIds = null); IFileInfo[] GetAudioFiles(string path, bool allDirectories = true); string[] GetNonAudioFiles(string path, bool allDirectories = true); List FilterFiles(string basePath, IEnumerable files); @@ -37,6 +40,7 @@ namespace NzbDrone.Core.MediaFiles public static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|^Thumbs\.db$|^\.DS_store$|\.partial~$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private readonly IDiskProvider _diskProvider; + private readonly ICalibreProxy _calibre; private readonly IMediaFileService _mediaFileService; private readonly IMakeImportDecision _importDecisionMaker; private readonly IImportApprovedTracks _importApprovedTracks; @@ -47,6 +51,7 @@ namespace NzbDrone.Core.MediaFiles private readonly Logger _logger; public DiskScanService(IDiskProvider diskProvider, + ICalibreProxy calibre, IMediaFileService mediaFileService, IMakeImportDecision importDecisionMaker, IImportApprovedTracks importApprovedTracks, @@ -57,6 +62,8 @@ namespace NzbDrone.Core.MediaFiles Logger logger) { _diskProvider = diskProvider; + _calibre = calibre; + _mediaFileService = mediaFileService; _importDecisionMaker = importDecisionMaker; _importApprovedTracks = importApprovedTracks; @@ -67,16 +74,16 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } - public void Scan(List folders = null, FilterFilesType filter = FilterFilesType.Known, bool addNewArtists = false, List artistIds = null) + public void Scan(List folders = null, FilterFilesType filter = FilterFilesType.Known, bool addNewArtists = false, List authorIds = null) { if (folders == null) { folders = _rootFolderService.All().Select(x => x.Path).ToList(); } - if (artistIds == null) + if (authorIds == null) { - artistIds = new List(); + authorIds = new List(); } var mediaFileList = new List(); @@ -99,7 +106,7 @@ namespace NzbDrone.Core.MediaFiles { _logger.Warn("Root folder {0} doesn't exist.", rootFolder.Path); - var skippedArtists = _artistService.GetArtists(artistIds); + var skippedArtists = _artistService.GetArtists(authorIds); skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderDoesNotExist))); return; } @@ -108,7 +115,7 @@ namespace NzbDrone.Core.MediaFiles { _logger.Warn("Root folder {0} is empty.", rootFolder.Path); - var skippedArtists = _artistService.GetArtists(artistIds); + var skippedArtists = _artistService.GetArtists(authorIds); skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderIsEmpty))); return; } @@ -158,14 +165,15 @@ namespace NzbDrone.Core.MediaFiles // decisions may have been filtered to just new files. Anything new and approved will have been inserted. // Now we need to make sure anything new but not approved gets inserted // Note that knownFiles will include anything imported just now - var knownFiles = new List(); + var knownFiles = new List(); folders.ForEach(x => knownFiles.AddRange(_mediaFileService.GetFilesWithBasePath(x))); var newFiles = decisions .ExceptBy(x => x.Item.Path, knownFiles, x => x.Path, PathEqualityComparer.Instance) - .Select(decision => new TrackFile + .Select(decision => new BookFile { Path = decision.Item.Path, + CalibreId = decision.Item.Path.ParseCalibreId(), Size = decision.Item.Size, Modified = decision.Item.Modified, DateAdded = DateTime.UtcNow, @@ -204,7 +212,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Debug($"Updated info for {updatedFiles.Count} known files"); - var artists = _artistService.GetArtists(artistIds); + var artists = _artistService.GetArtists(authorIds); foreach (var artist in artists) { CompletedScanning(artist); @@ -220,7 +228,7 @@ namespace NzbDrone.Core.MediaFiles _mediaFileTableCleanupService.Clean(folder, mediaFileList); } - private void CompletedScanning(Artist artist) + private void CompletedScanning(Author artist) { _logger.Info("Completed scanning disk for {0}", artist.Name); _eventAggregator.PublishEvent(new ArtistScannedEvent(artist)); @@ -228,18 +236,36 @@ namespace NzbDrone.Core.MediaFiles public IFileInfo[] GetAudioFiles(string path, bool allDirectories = true) { - _logger.Debug("Scanning '{0}' for music files", path); + IEnumerable filesOnDisk; - var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - var filesOnDisk = _diskProvider.GetFileInfos(path, searchOption); + var rootFolder = _rootFolderService.GetBestRootFolder(path); - var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(file.Extension)) - .ToList(); + _logger.Trace(rootFolder.ToJson()); - _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); - _logger.Debug("{0} audio files were found in {1}", mediaFileList.Count, path); + if (rootFolder != null && rootFolder.IsCalibreLibrary && rootFolder.CalibreSettings != null) + { + _logger.Info($"Getting book list from calibre for {path}"); + var paths = _calibre.GetAllBookFilePaths(rootFolder.CalibreSettings); + var folderPaths = paths.Where(x => path.IsParentPath(x)); - return mediaFileList.ToArray(); + filesOnDisk = folderPaths.Select(x => _diskProvider.GetFileInfo(x)); + } + else + { + _logger.Debug("Scanning '{0}' for music files", path); + + var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + filesOnDisk = _diskProvider.GetFileInfos(path, searchOption); + + _logger.Trace("{0} files were found in {1}", filesOnDisk.Count(), path); + } + + var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.AllExtensions.Contains(file.Extension)) + .ToArray(); + + _logger.Debug("{0} book files were found in {1}", mediaFileList.Length, path); + + return mediaFileList; } public string[] GetNonAudioFiles(string path, bool allDirectories = true) @@ -249,7 +275,7 @@ namespace NzbDrone.Core.MediaFiles var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList(); - var mediaFileList = filesOnDisk.Where(file => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(file))) + var mediaFileList = filesOnDisk.Where(file => !MediaFileExtensions.AllExtensions.Contains(Path.GetExtension(file))) .ToList(); _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); @@ -274,7 +300,7 @@ namespace NzbDrone.Core.MediaFiles public void Execute(RescanFoldersCommand message) { - Scan(message.Folders, message.Filter, message.AddNewArtists, message.ArtistIds); + Scan(message.Folders, message.Filter, message.AddNewArtists, message.AuthorIds); } } } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs index baa2cd61f..83eefcbd7 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs @@ -20,8 +20,8 @@ namespace NzbDrone.Core.MediaFiles public interface IDownloadedTracksImportService { List ProcessRootFolder(IDirectoryInfo directoryInfo); - List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Artist artist = null, DownloadClientItem downloadClientItem = null); - bool ShouldDeleteFolder(IDirectoryInfo directoryInfo, Artist artist); + List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Author artist = null, DownloadClientItem downloadClientItem = null); + bool ShouldDeleteFolder(IDirectoryInfo directoryInfo, Author artist); } public class DownloadedTracksImportService : IDownloadedTracksImportService @@ -76,7 +76,7 @@ namespace NzbDrone.Core.MediaFiles return results; } - public List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Artist artist = null, DownloadClientItem downloadClientItem = null) + public List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Author artist = null, DownloadClientItem downloadClientItem = null) { if (_diskProvider.FolderExists(path)) { @@ -108,7 +108,7 @@ namespace NzbDrone.Core.MediaFiles return new List(); } - public bool ShouldDeleteFolder(IDirectoryInfo directoryInfo, Artist artist) + public bool ShouldDeleteFolder(IDirectoryInfo directoryInfo, Author artist) { var audioFiles = _diskScanService.GetAudioFiles(directoryInfo.FullName); var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f).Equals(".rar", StringComparison.OrdinalIgnoreCase)); @@ -154,7 +154,7 @@ namespace NzbDrone.Core.MediaFiles return ProcessFolder(directoryInfo, importMode, artist, downloadClientItem); } - private List ProcessFolder(IDirectoryInfo directoryInfo, ImportMode importMode, Artist artist, DownloadClientItem downloadClientItem) + private List ProcessFolder(IDirectoryInfo directoryInfo, ImportMode importMode, Author artist, DownloadClientItem downloadClientItem) { if (_artistService.ArtistPathExists(directoryInfo.FullName)) { @@ -254,7 +254,7 @@ namespace NzbDrone.Core.MediaFiles return ProcessFile(fileInfo, importMode, artist, downloadClientItem); } - private List ProcessFile(IFileInfo fileInfo, ImportMode importMode, Artist artist, DownloadClientItem downloadClientItem) + private List ProcessFile(IFileInfo fileInfo, ImportMode importMode, Author artist, DownloadClientItem downloadClientItem) { if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._")) { diff --git a/src/NzbDrone.Core/MediaFiles/EbookTagService.cs b/src/NzbDrone.Core/MediaFiles/EbookTagService.cs new file mode 100644 index 000000000..0819d12ce --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EbookTagService.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using NLog; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.MediaFiles.Azw; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using PdfSharpCore.Pdf.IO; +using VersOne.Epub; +using VersOne.Epub.Schema; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IEBookTagService + { + ParsedTrackInfo ReadTags(IFileInfo file); + } + + public class EBookTagService : IEBookTagService + { + private readonly Logger _logger; + + public EBookTagService(Logger logger) + { + _logger = logger; + } + + public ParsedTrackInfo ReadTags(IFileInfo file) + { + var extension = file.Extension.ToLower(); + _logger.Trace($"Got extension '{extension}'"); + + switch (extension) + { + case ".pdf": + return ReadPdf(file.FullName); + case ".epub": + return ReadEpub(file.FullName); + case ".azw3": + case ".mobi": + return ReadAzw3(file.FullName); + default: + return Parser.Parser.ParseMusicTitle(file.FullName); + } + } + + private ParsedTrackInfo ReadEpub(string file) + { + _logger.Trace($"Reading {file}"); + var result = new ParsedTrackInfo + { + Quality = new QualityModel + { + Quality = Quality.EPUB, + QualityDetectionSource = QualityDetectionSource.TagLib + } + }; + + try + { + using (var bookRef = EpubReader.OpenBook(file)) + { + result.ArtistTitle = bookRef.AuthorList.FirstOrDefault(); + result.AlbumTitle = bookRef.Title; + + var meta = bookRef.Schema.Package.Metadata; + + _logger.Trace(meta.ToJson()); + + result.Isbn = GetIsbn(meta?.Identifiers); + result.Asin = meta?.Identifiers?.FirstOrDefault(x => x.Scheme?.ToLower().Contains("asin") ?? false)?.Identifier; + result.Language = meta?.Languages?.FirstOrDefault(); + result.Publisher = meta?.Publishers?.FirstOrDefault(); + result.Disambiguation = meta?.Description; + + result.SeriesTitle = meta?.MetaItems?.FirstOrDefault(x => x.Name == "calibre:series")?.Content; + result.SeriesIndex = meta?.MetaItems?.FirstOrDefault(x => x.Name == "calibre:series_index")?.Content; + } + } + catch (Exception e) + { + _logger.Error(e, "Error reading epub"); + result.Quality.QualityDetectionSource = QualityDetectionSource.Extension; + } + + _logger.Trace($"Got:\n{result.ToJson()}"); + + return result; + } + + private ParsedTrackInfo ReadAzw3(string file) + { + _logger.Trace($"Reading {file}"); + var result = new ParsedTrackInfo(); + + try + { + var book = new Azw3File(file); + result.ArtistTitle = book.Author; + result.AlbumTitle = book.Title; + result.Isbn = StripIsbn(book.Isbn); + result.Asin = book.Asin; + result.Language = book.Language; + result.Disambiguation = book.Description; + result.Publisher = book.Publisher; + result.Label = book.Imprint; + result.Source = book.Source; + + result.Quality = new QualityModel + { + Quality = book.Version <= 6 ? Quality.MOBI : Quality.AZW3, + QualityDetectionSource = QualityDetectionSource.TagLib + }; + } + catch (Exception e) + { + _logger.Error(e, "Error reading file"); + + result.Quality = new QualityModel + { + Quality = Path.GetExtension(file) == ".mobi" ? Quality.MOBI : Quality.AZW3, + QualityDetectionSource = QualityDetectionSource.Extension + }; + } + + _logger.Trace($"Got {result.ToJson()}"); + + return result; + } + + private ParsedTrackInfo ReadPdf(string file) + { + _logger.Trace($"Reading {file}"); + var result = new ParsedTrackInfo + { + Quality = new QualityModel + { + Quality = Quality.PDF, + QualityDetectionSource = QualityDetectionSource.TagLib + } + }; + + try + { + var book = PdfReader.Open(file, PdfDocumentOpenMode.InformationOnly); + result.ArtistTitle = book.Info.Author; + result.AlbumTitle = book.Info.Title; + + _logger.Trace(book.Info.ToJson()); + _logger.Trace(book.CustomValues.ToJson()); + } + catch (Exception e) + { + _logger.Error(e, "Error reading pdf"); + result.Quality.QualityDetectionSource = QualityDetectionSource.Extension; + } + + _logger.Trace($"Got:\n{result.ToJson()}"); + + return result; + } + + private string GetIsbn(IEnumerable ids) + { + foreach (var id in ids) + { + var isbn = StripIsbn(id?.Identifier); + if (isbn != null) + { + return isbn; + } + } + + return null; + } + + private string GetIsbnChars(string input) + { + if (input == null) + { + return null; + } + + return new string(input.Where(c => char.IsDigit(c) || c == 'X' || c == 'x').ToArray()); + } + + private string StripIsbn(string input) + { + var isbn = GetIsbnChars(input); + + if (isbn == null) + { + return null; + } + else if ((isbn.Length == 10 && ValidateIsbn10(isbn)) || + (isbn.Length == 13 && ValidateIsbn13(isbn))) + { + return isbn; + } + + return null; + } + + private static char Isbn10Checksum(string isbn) + { + var sum = 0; + for (var i = 0; i < 9; i++) + { + sum += int.Parse(isbn[i].ToString()) * (10 - i); + } + + var result = sum % 11; + + if (result == 0) + { + return '0'; + } + else if (result == 1) + { + return 'X'; + } + + return (11 - result).ToString()[0]; + } + + private static char Isbn13Checksum(string isbn) + { + var result = 0; + for (var i = 0; i < 12; i++) + { + result += int.Parse(isbn[i].ToString()) * ((i % 2 == 0) ? 1 : 3); + } + + result %= 10; + + return result == 0 ? '0' : (10 - result).ToString()[0]; + } + + private static bool ValidateIsbn10(string isbn) + { + return ulong.TryParse(isbn.Substring(0, 9), out _) && isbn[9] == Isbn10Checksum(isbn); + } + + private static bool ValidateIsbn13(string isbn) + { + return ulong.TryParse(isbn, out _) && isbn[12] == Isbn13Checksum(isbn); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Entities/EpubBook.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Entities/EpubBook.cs new file mode 100644 index 000000000..d42d630b7 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Entities/EpubBook.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace VersOne.Epub +{ + public class EpubBook + { + public string FilePath { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public List AuthorList { get; set; } + public EpubSchema Schema { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Entities/EpubSchema.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Entities/EpubSchema.cs new file mode 100644 index 000000000..66dc2659d --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Entities/EpubSchema.cs @@ -0,0 +1,10 @@ +using VersOne.Epub.Schema; + +namespace VersOne.Epub +{ + public class EpubSchema + { + public EpubPackage Package { get; set; } + public string ContentDirectoryPath { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/EpubReader.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/EpubReader.cs new file mode 100644 index 000000000..e7919e42f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/EpubReader.cs @@ -0,0 +1,69 @@ +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using VersOne.Epub.Internal; + +namespace VersOne.Epub +{ + public static class EpubReader + { + /// + /// Opens the book synchronously without reading its whole content. Holds the handle to the EPUB file. + /// + /// path to the EPUB file + /// + public static EpubBookRef OpenBook(string filePath) + { + return OpenBookAsync(filePath).Result; + } + + /// + /// Opens the book asynchronously without reading its whole content. Holds the handle to the EPUB file. + /// + /// path to the EPUB file + /// + public static Task OpenBookAsync(string filePath) + { + if (!File.Exists(filePath)) + { + if (!filePath.StartsWith(@"\\?\")) + { + filePath = @"\\?\" + filePath; + } + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException("Specified epub file not found.", filePath); + } + } + + return OpenBookAsync(GetZipArchive(filePath)); + } + + private static async Task OpenBookAsync(ZipArchive zipArchive, string filePath = null) + { + EpubBookRef result = null; + try + { + result = new EpubBookRef(zipArchive); + result.FilePath = filePath; + result.Schema = await SchemaReader.ReadSchemaAsync(zipArchive).ConfigureAwait(false); + result.Title = result.Schema.Package.Metadata.Titles.FirstOrDefault() ?? string.Empty; + result.AuthorList = result.Schema.Package.Metadata.Creators.Select(creator => creator.Creator).ToList(); + result.Author = string.Join(", ", result.AuthorList); + return result; + } + catch + { + result?.Dispose(); + throw; + } + } + + private static ZipArchive GetZipArchive(string filePath) + { + return ZipFile.OpenRead(filePath); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Readers/PackageReader.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Readers/PackageReader.cs new file mode 100644 index 000000000..c8359172b --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Readers/PackageReader.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.IO.Compression; +using System.Threading.Tasks; +using System.Xml.Linq; +using VersOne.Epub.Schema; +using VersOne.Epub.Utils; + +namespace VersOne.Epub.Internal +{ + public static class PackageReader + { + public static async Task ReadPackageAsync(ZipArchive epubArchive, string rootFilePath) + { + var rootFileEntry = epubArchive.GetEntry(rootFilePath); + if (rootFileEntry == null) + { + throw new Exception("EPUB parsing error: root file not found in archive."); + } + + XDocument containerDocument; + + using (var containerStream = rootFileEntry.Open()) + { + containerDocument = await XmlUtils.LoadDocumentAsync(containerStream).ConfigureAwait(false); + } + + XNamespace opfNamespace = "http://www.idpf.org/2007/opf"; + var packageNode = containerDocument.Element(opfNamespace + "package"); + var result = new EpubPackage(); + var epubVersionValue = packageNode.Attribute("version").Value; + EpubVersion epubVersion; + switch (epubVersionValue) + { + case "1.0": + case "2.0": + epubVersion = EpubVersion.EPUB_2; + break; + case "3.0": + epubVersion = EpubVersion.EPUB_3_0; + break; + case "3.1": + epubVersion = EpubVersion.EPUB_3_1; + break; + default: + throw new Exception($"Unsupported EPUB version: {epubVersionValue}."); + } + + result.EpubVersion = epubVersion; + var metadataNode = packageNode.Element(opfNamespace + "metadata"); + if (metadataNode == null) + { + throw new Exception("EPUB parsing error: metadata not found in the package."); + } + + var metadata = ReadMetadata(metadataNode, result.EpubVersion); + result.Metadata = metadata; + + return result; + } + + private static EpubMetadata ReadMetadata(XElement metadataNode, EpubVersion epubVersion) + { + var result = new EpubMetadata + { + Titles = new List(), + Creators = new List(), + Subjects = new List(), + Publishers = new List(), + Contributors = new List(), + Dates = new List(), + Types = new List(), + Formats = new List(), + Identifiers = new List(), + Sources = new List(), + Languages = new List(), + Relations = new List(), + Coverages = new List(), + Rights = new List(), + MetaItems = new List() + }; + + foreach (var metadataItemNode in metadataNode.Elements()) + { + var innerText = metadataItemNode.Value; + switch (metadataItemNode.GetLowerCaseLocalName()) + { + case "title": + result.Titles.Add(innerText); + break; + case "creator": + var creator = ReadMetadataCreator(metadataItemNode); + result.Creators.Add(creator); + break; + case "subject": + result.Subjects.Add(innerText); + break; + case "description": + result.Description = innerText; + break; + case "publisher": + result.Publishers.Add(innerText); + break; + case "contributor": + var contributor = ReadMetadataContributor(metadataItemNode); + result.Contributors.Add(contributor); + break; + case "date": + var date = ReadMetadataDate(metadataItemNode); + result.Dates.Add(date); + break; + case "type": + result.Types.Add(innerText); + break; + case "format": + result.Formats.Add(innerText); + break; + case "identifier": + var identifier = ReadMetadataIdentifier(metadataItemNode); + result.Identifiers.Add(identifier); + break; + case "source": + result.Sources.Add(innerText); + break; + case "language": + result.Languages.Add(innerText); + break; + case "relation": + result.Relations.Add(innerText); + break; + case "coverage": + result.Coverages.Add(innerText); + break; + case "rights": + result.Rights.Add(innerText); + break; + case "meta": + if (epubVersion == EpubVersion.EPUB_2) + { + var meta = ReadMetadataMetaVersion2(metadataItemNode); + result.MetaItems.Add(meta); + } + else if (epubVersion == EpubVersion.EPUB_3_0 || epubVersion == EpubVersion.EPUB_3_1) + { + var meta = ReadMetadataMetaVersion3(metadataItemNode); + result.MetaItems.Add(meta); + } + + break; + } + } + + return result; + } + + private static EpubMetadataCreator ReadMetadataCreator(XElement metadataCreatorNode) + { + var result = new EpubMetadataCreator(); + foreach (var metadataCreatorNodeAttribute in metadataCreatorNode.Attributes()) + { + var attributeValue = metadataCreatorNodeAttribute.Value; + switch (metadataCreatorNodeAttribute.GetLowerCaseLocalName()) + { + case "role": + result.Role = attributeValue; + break; + case "file-as": + result.FileAs = attributeValue; + break; + } + } + + result.Creator = metadataCreatorNode.Value; + return result; + } + + private static EpubMetadataContributor ReadMetadataContributor(XElement metadataContributorNode) + { + var result = new EpubMetadataContributor(); + foreach (var metadataContributorNodeAttribute in metadataContributorNode.Attributes()) + { + var attributeValue = metadataContributorNodeAttribute.Value; + switch (metadataContributorNodeAttribute.GetLowerCaseLocalName()) + { + case "role": + result.Role = attributeValue; + break; + case "file-as": + result.FileAs = attributeValue; + break; + } + } + + result.Contributor = metadataContributorNode.Value; + return result; + } + + private static EpubMetadataDate ReadMetadataDate(XElement metadataDateNode) + { + var result = new EpubMetadataDate(); + var eventAttribute = metadataDateNode.Attribute(metadataDateNode.Name.Namespace + "event"); + if (eventAttribute != null) + { + result.Event = eventAttribute.Value; + } + + result.Date = metadataDateNode.Value; + + return result; + } + + private static EpubMetadataIdentifier ReadMetadataIdentifier(XElement metadataIdentifierNode) + { + var result = new EpubMetadataIdentifier(); + foreach (var metadataIdentifierNodeAttribute in metadataIdentifierNode.Attributes()) + { + var attributeValue = metadataIdentifierNodeAttribute.Value; + switch (metadataIdentifierNodeAttribute.GetLowerCaseLocalName()) + { + case "id": + result.Id = attributeValue; + break; + case "opf:scheme": + result.Scheme = attributeValue; + break; + } + } + + result.Identifier = metadataIdentifierNode.Value; + return result; + } + + private static EpubMetadataMeta ReadMetadataMetaVersion2(XElement metadataMetaNode) + { + var result = new EpubMetadataMeta(); + foreach (var metadataMetaNodeAttribute in metadataMetaNode.Attributes()) + { + var attributeValue = metadataMetaNodeAttribute.Value; + switch (metadataMetaNodeAttribute.GetLowerCaseLocalName()) + { + case "name": + result.Name = attributeValue; + break; + case "content": + result.Content = attributeValue; + break; + } + } + + return result; + } + + private static EpubMetadataMeta ReadMetadataMetaVersion3(XElement metadataMetaNode) + { + var result = new EpubMetadataMeta(); + foreach (var metadataMetaNodeAttribute in metadataMetaNode.Attributes()) + { + var attributeValue = metadataMetaNodeAttribute.Value; + switch (metadataMetaNodeAttribute.GetLowerCaseLocalName()) + { + case "id": + result.Id = attributeValue; + break; + case "refines": + result.Refines = attributeValue; + break; + case "property": + result.Property = attributeValue; + break; + case "scheme": + result.Scheme = attributeValue; + break; + } + } + + result.Content = metadataMetaNode.Value; + return result; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Readers/RootFilePathReader.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Readers/RootFilePathReader.cs new file mode 100644 index 000000000..bbd62dd68 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Readers/RootFilePathReader.cs @@ -0,0 +1,35 @@ +using System; +using System.IO.Compression; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace VersOne.Epub.Internal +{ + public static class RootFilePathReader + { + public static async Task GetRootFilePathAsync(ZipArchive epubArchive) + { + const string EPUB_CONTAINER_FILE_PATH = "META-INF/container.xml"; + var containerFileEntry = epubArchive.GetEntry(EPUB_CONTAINER_FILE_PATH); + if (containerFileEntry == null) + { + throw new Exception($"EPUB parsing error: {EPUB_CONTAINER_FILE_PATH} file not found in archive."); + } + + XDocument containerDocument; + using (var containerStream = containerFileEntry.Open()) + { + containerDocument = await XmlUtils.LoadDocumentAsync(containerStream).ConfigureAwait(false); + } + + XNamespace cnsNamespace = "urn:oasis:names:tc:opendocument:xmlns:container"; + var fullPathAttribute = containerDocument.Element(cnsNamespace + "container")?.Element(cnsNamespace + "rootfiles")?.Element(cnsNamespace + "rootfile")?.Attribute("full-path"); + if (fullPathAttribute == null) + { + throw new Exception("EPUB parsing error: root file path not found in the EPUB container."); + } + + return fullPathAttribute.Value; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Readers/SchemaReader.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Readers/SchemaReader.cs new file mode 100644 index 000000000..1d2efd16b --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Readers/SchemaReader.cs @@ -0,0 +1,20 @@ +using System.IO.Compression; +using System.Threading.Tasks; +using VersOne.Epub.Schema; + +namespace VersOne.Epub.Internal +{ + public static class SchemaReader + { + public static async Task ReadSchemaAsync(ZipArchive epubArchive) + { + var result = new EpubSchema(); + var rootFilePath = await RootFilePathReader.GetRootFilePathAsync(epubArchive).ConfigureAwait(false); + var contentDirectoryPath = ZipPathUtils.GetDirectoryPath(rootFilePath); + result.ContentDirectoryPath = contentDirectoryPath; + EpubPackage package = await PackageReader.ReadPackageAsync(epubArchive, rootFilePath).ConfigureAwait(false); + result.Package = package; + return result; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/RefEntities/EpubBookRef.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/RefEntities/EpubBookRef.cs new file mode 100644 index 000000000..bacad1082 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/RefEntities/EpubBookRef.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO.Compression; + +namespace VersOne.Epub +{ + public class EpubBookRef : IDisposable + { + private bool _isDisposed; + + public EpubBookRef(ZipArchive epubArchive) + { + EpubArchive = epubArchive; + _isDisposed = false; + } + + ~EpubBookRef() + { + Dispose(false); + } + + public string FilePath { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public List AuthorList { get; set; } + public EpubSchema Schema { get; set; } + + protected ZipArchive EpubArchive { get; private set; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + EpubArchive?.Dispose(); + } + + _isDisposed = true; + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Common/ManifestProperty.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Common/ManifestProperty.cs new file mode 100644 index 000000000..3b4f04516 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Common/ManifestProperty.cs @@ -0,0 +1,37 @@ +namespace VersOne.Epub.Schema +{ + public enum ManifestProperty + { + COVER_IMAGE = 1, + MATHML, + NAV, + REMOTE_RESOURCES, + SCRIPTED, + SVG, + UNKNOWN + } + + public static class ManifestPropertyParser + { + public static ManifestProperty Parse(string stringValue) + { + switch (stringValue.ToLowerInvariant()) + { + case "cover-image": + return ManifestProperty.COVER_IMAGE; + case "mathml": + return ManifestProperty.MATHML; + case "nav": + return ManifestProperty.NAV; + case "remote-resources": + return ManifestProperty.REMOTE_RESOURCES; + case "scripted": + return ManifestProperty.SCRIPTED; + case "svg": + return ManifestProperty.SVG; + default: + return ManifestProperty.UNKNOWN; + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Common/StructuralSemanticsProperty.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Common/StructuralSemanticsProperty.cs new file mode 100644 index 000000000..30410229a --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Common/StructuralSemanticsProperty.cs @@ -0,0 +1,409 @@ +namespace VersOne.Epub.Schema +{ + public enum StructuralSemanticsProperty + { + COVER = 1, + FRONTMATTER, + BODYMATTER, + BACKMATTER, + VOLUME, + PART, + CHAPTER, + SUBCHAPTER, + DIVISION, + ABSTRACT, + FOREWORD, + PREFACE, + PROLOGUE, + INTRODUCTION, + PREAMBLE, + CONCLUSION, + EPILOGUE, + AFTERWORD, + EPIGRAPH, + TOC, + TOC_BRIEF, + LANDMARKS, + LOA, + LOI, + LOT, + LOV, + APPENDIX, + COLOPHON, + CREDITS, + KEYWORDS, + INDEX, + INDEX_HEADNOTES, + INDEX_LEGEND, + INDEX_GROUP, + INDEX_ENTRY_LIST, + INDEX_ENTRY, + INDEX_TERM, + INDEX_EDITOR_NOTE, + INDEX_LOCATOR, + INDEX_LOCATOR_LIST, + INDEX_LOCATOR_RANGE, + INDEX_XREF_PREFERRED, + INDEX_XREF_RELATED, + INDEX_TERM_CATEGORY, + INDEX_TERM_CATEGORIES, + GLOSSARY, + GLOSSTERM, + GLOSSDEF, + BIBLIOGRAPHY, + BIBLIOENTRY, + TITLEPAGE, + HALFTITLEPAGE, + COPYRIGHT_PAGE, + SERIESPAGE, + ACKNOWLEDGMENTS, + IMPRINT, + IMPRIMATUR, + CONTRIBUTORS, + OTHER_CREDITS, + ERRATA, + DEDICATION, + REVISION_HISTORY, + CASE_STUDY, + HELP, + MARGINALIA, + NOTICE, + PULLQUOTE, + SIDEBAR, + TIP, + WARNING, + HALFTITLE, + FULLTITLE, + COVERTITLE, + TITLE, + SUBTITLE, + LABEL, + ORDINAL, + BRIDGEHEAD, + LEARNING_OBJECTIVE, + LEARNING_OBJECTIVES, + LEARNING_OUTCOME, + LEARNING_OUTCOMES, + LEARNING_RESOURCE, + LEARNING_RESOURCES, + LEARNING_STANDARD, + LEARNING_STANDARDS, + ANSWER, + ANSWERS, + ASSESSMENT, + ASSESSMENTS, + FEEDBACK, + FILL_IN_THE_BLANK_PROBLEM, + GENERAL_PROBLEM, + QNA, + MATCH_PROBLEM, + MULTIPLE_CHOICE_PROBLEM, + PRACTICE, + QUESTION, + PRACTICES, + TRUE_FALSE_PROBLEM, + PANEL, + PANEL_GROUP, + BALLOON, + TEXT_AREA, + SOUND_AREA, + ANNOTATION, + NOTE, + FOOTNOTE, + ENDNOTE, + REARNOTE, + FOOTNOTES, + ENDNOTES, + REARNOTES, + ANNOREF, + BIBLIOREF, + GLOSSREF, + NOTEREF, + BACKLINK, + CREDIT, + KEYWORD, + TOPIC_SENTENCE, + CONCLUDING_SENTENCE, + PAGEBREAK, + PAGE_LIST, + TABLE, + TABLE_ROW, + TABLE_CELL, + LIST, + LIST_ITEM, + FIGURE, + UNKNOWN + } + + internal static class StructuralSemanticsPropertyParser + { + public static StructuralSemanticsProperty Parse(string stringValue) + { + switch (stringValue.ToLowerInvariant()) + { + case "cover": + return StructuralSemanticsProperty.COVER; + case "frontmatter": + return StructuralSemanticsProperty.FRONTMATTER; + case "bodymatter": + return StructuralSemanticsProperty.BODYMATTER; + case "backmatter": + return StructuralSemanticsProperty.BACKMATTER; + case "volume": + return StructuralSemanticsProperty.VOLUME; + case "part": + return StructuralSemanticsProperty.PART; + case "chapter": + return StructuralSemanticsProperty.CHAPTER; + case "subchapter": + return StructuralSemanticsProperty.SUBCHAPTER; + case "division": + return StructuralSemanticsProperty.DIVISION; + case "abstract": + return StructuralSemanticsProperty.ABSTRACT; + case "foreword": + return StructuralSemanticsProperty.FOREWORD; + case "preface": + return StructuralSemanticsProperty.PREFACE; + case "prologue": + return StructuralSemanticsProperty.PROLOGUE; + case "introduction": + return StructuralSemanticsProperty.INTRODUCTION; + case "preamble": + return StructuralSemanticsProperty.PREAMBLE; + case "conclusion": + return StructuralSemanticsProperty.CONCLUSION; + case "epilogue": + return StructuralSemanticsProperty.EPILOGUE; + case "afterword": + return StructuralSemanticsProperty.AFTERWORD; + case "epigraph": + return StructuralSemanticsProperty.EPIGRAPH; + case "toc": + return StructuralSemanticsProperty.TOC; + case "toc-brief": + return StructuralSemanticsProperty.TOC_BRIEF; + case "landmarks": + return StructuralSemanticsProperty.LANDMARKS; + case "loa": + return StructuralSemanticsProperty.LOA; + case "loi": + return StructuralSemanticsProperty.LOI; + case "lot": + return StructuralSemanticsProperty.LOT; + case "lov": + return StructuralSemanticsProperty.LOV; + case "appendix": + return StructuralSemanticsProperty.APPENDIX; + case "colophon": + return StructuralSemanticsProperty.COLOPHON; + case "credits": + return StructuralSemanticsProperty.CREDITS; + case "keywords": + return StructuralSemanticsProperty.KEYWORDS; + case "index": + return StructuralSemanticsProperty.INDEX; + case "index-headnotes": + return StructuralSemanticsProperty.INDEX_HEADNOTES; + case "index-legend": + return StructuralSemanticsProperty.INDEX_LEGEND; + case "index-group": + return StructuralSemanticsProperty.INDEX_GROUP; + case "index-entry-list": + return StructuralSemanticsProperty.INDEX_ENTRY_LIST; + case "index-entry": + return StructuralSemanticsProperty.INDEX_ENTRY; + case "index-term": + return StructuralSemanticsProperty.INDEX_TERM; + case "index-editor-note": + return StructuralSemanticsProperty.INDEX_EDITOR_NOTE; + case "index-locator": + return StructuralSemanticsProperty.INDEX_LOCATOR; + case "index-locator-list": + return StructuralSemanticsProperty.INDEX_LOCATOR_LIST; + case "index-locator-range": + return StructuralSemanticsProperty.INDEX_LOCATOR_RANGE; + case "index-xref-preferred": + return StructuralSemanticsProperty.INDEX_XREF_PREFERRED; + case "index-xref-related": + return StructuralSemanticsProperty.INDEX_XREF_RELATED; + case "index-term-category": + return StructuralSemanticsProperty.INDEX_TERM_CATEGORY; + case "index-term-categories": + return StructuralSemanticsProperty.INDEX_TERM_CATEGORIES; + case "glossary": + return StructuralSemanticsProperty.GLOSSARY; + case "glossterm": + return StructuralSemanticsProperty.GLOSSTERM; + case "glossdef": + return StructuralSemanticsProperty.GLOSSDEF; + case "bibliography": + return StructuralSemanticsProperty.BIBLIOGRAPHY; + case "biblioentry": + return StructuralSemanticsProperty.BIBLIOENTRY; + case "titlepage": + return StructuralSemanticsProperty.TITLEPAGE; + case "halftitlepage": + return StructuralSemanticsProperty.HALFTITLEPAGE; + case "copyright-page": + return StructuralSemanticsProperty.COPYRIGHT_PAGE; + case "seriespage": + return StructuralSemanticsProperty.SERIESPAGE; + case "acknowledgments": + return StructuralSemanticsProperty.ACKNOWLEDGMENTS; + case "imprint": + return StructuralSemanticsProperty.IMPRINT; + case "imprimatur": + return StructuralSemanticsProperty.IMPRIMATUR; + case "contributors": + return StructuralSemanticsProperty.CONTRIBUTORS; + case "other-credits": + return StructuralSemanticsProperty.OTHER_CREDITS; + case "errata": + return StructuralSemanticsProperty.ERRATA; + case "dedication": + return StructuralSemanticsProperty.DEDICATION; + case "revision-history": + return StructuralSemanticsProperty.REVISION_HISTORY; + case "case-study": + return StructuralSemanticsProperty.CASE_STUDY; + case "help": + return StructuralSemanticsProperty.HELP; + case "marginalia": + return StructuralSemanticsProperty.MARGINALIA; + case "notice": + return StructuralSemanticsProperty.NOTICE; + case "pullquote": + return StructuralSemanticsProperty.PULLQUOTE; + case "sidebar": + return StructuralSemanticsProperty.SIDEBAR; + case "tip": + return StructuralSemanticsProperty.TIP; + case "warning": + return StructuralSemanticsProperty.WARNING; + case "halftitle": + return StructuralSemanticsProperty.HALFTITLE; + case "fulltitle": + return StructuralSemanticsProperty.FULLTITLE; + case "covertitle": + return StructuralSemanticsProperty.COVERTITLE; + case "title": + return StructuralSemanticsProperty.TITLE; + case "subtitle": + return StructuralSemanticsProperty.SUBTITLE; + case "label": + return StructuralSemanticsProperty.LABEL; + case "ordinal": + return StructuralSemanticsProperty.ORDINAL; + case "bridgehead": + return StructuralSemanticsProperty.BRIDGEHEAD; + case "learning-objective": + return StructuralSemanticsProperty.LEARNING_OBJECTIVE; + case "learning-objectives": + return StructuralSemanticsProperty.LEARNING_OBJECTIVES; + case "learning-outcome": + return StructuralSemanticsProperty.LEARNING_OUTCOME; + case "learning-outcomes": + return StructuralSemanticsProperty.LEARNING_OUTCOMES; + case "learning-resource": + return StructuralSemanticsProperty.LEARNING_RESOURCE; + case "learning-resources": + return StructuralSemanticsProperty.LEARNING_RESOURCES; + case "learning-standard": + return StructuralSemanticsProperty.LEARNING_STANDARD; + case "learning-standards": + return StructuralSemanticsProperty.LEARNING_STANDARDS; + case "answer": + return StructuralSemanticsProperty.ANSWER; + case "answers": + return StructuralSemanticsProperty.ANSWERS; + case "assessment": + return StructuralSemanticsProperty.ASSESSMENT; + case "assessments": + return StructuralSemanticsProperty.ASSESSMENTS; + case "feedback": + return StructuralSemanticsProperty.FEEDBACK; + case "fill-in-the-blank-problem": + return StructuralSemanticsProperty.FILL_IN_THE_BLANK_PROBLEM; + case "general-problem": + return StructuralSemanticsProperty.GENERAL_PROBLEM; + case "qna": + return StructuralSemanticsProperty.QNA; + case "match-problem": + return StructuralSemanticsProperty.MATCH_PROBLEM; + case "multiple-choice-problem": + return StructuralSemanticsProperty.MULTIPLE_CHOICE_PROBLEM; + case "practice": + return StructuralSemanticsProperty.PRACTICE; + case "question": + return StructuralSemanticsProperty.QUESTION; + case "practices": + return StructuralSemanticsProperty.PRACTICES; + case "true-false-problem": + return StructuralSemanticsProperty.TRUE_FALSE_PROBLEM; + case "panel": + return StructuralSemanticsProperty.PANEL; + case "panel-group": + return StructuralSemanticsProperty.PANEL_GROUP; + case "balloon": + return StructuralSemanticsProperty.BALLOON; + case "text-area": + return StructuralSemanticsProperty.TEXT_AREA; + case "sound-area": + return StructuralSemanticsProperty.SOUND_AREA; + case "annotation": + return StructuralSemanticsProperty.ANNOTATION; + case "note": + return StructuralSemanticsProperty.NOTE; + case "footnote": + return StructuralSemanticsProperty.FOOTNOTE; + case "endnote": + return StructuralSemanticsProperty.ENDNOTE; + case "rearnote": + return StructuralSemanticsProperty.REARNOTE; + case "footnotes": + return StructuralSemanticsProperty.FOOTNOTES; + case "endnotes": + return StructuralSemanticsProperty.ENDNOTES; + case "rearnotes": + return StructuralSemanticsProperty.REARNOTES; + case "annoref": + return StructuralSemanticsProperty.ANNOREF; + case "biblioref": + return StructuralSemanticsProperty.BIBLIOREF; + case "glossref": + return StructuralSemanticsProperty.GLOSSREF; + case "noteref": + return StructuralSemanticsProperty.NOTEREF; + case "backlink": + return StructuralSemanticsProperty.BACKLINK; + case "credit": + return StructuralSemanticsProperty.CREDIT; + case "keyword": + return StructuralSemanticsProperty.KEYWORD; + case "topic-sentence": + return StructuralSemanticsProperty.TOPIC_SENTENCE; + case "concluding-sentence": + return StructuralSemanticsProperty.CONCLUDING_SENTENCE; + case "pagebreak": + return StructuralSemanticsProperty.PAGEBREAK; + case "page-list": + return StructuralSemanticsProperty.PAGE_LIST; + case "table": + return StructuralSemanticsProperty.TABLE; + case "table-row": + return StructuralSemanticsProperty.TABLE_ROW; + case "table-cell": + return StructuralSemanticsProperty.TABLE_CELL; + case "list": + return StructuralSemanticsProperty.LIST; + case "list-item": + return StructuralSemanticsProperty.LIST_ITEM; + case "figure": + return StructuralSemanticsProperty.FIGURE; + default: + return StructuralSemanticsProperty.UNKNOWN; + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadata.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadata.cs new file mode 100644 index 000000000..e27f77a7c --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadata.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace VersOne.Epub.Schema +{ + public class EpubMetadata + { + public List Titles { get; set; } + public List Creators { get; set; } + public List Subjects { get; set; } + public string Description { get; set; } + public List Publishers { get; set; } + public List Contributors { get; set; } + public List Dates { get; set; } + public List Types { get; set; } + public List Formats { get; set; } + public List Identifiers { get; set; } + public List Sources { get; set; } + public List Languages { get; set; } + public List Relations { get; set; } + public List Coverages { get; set; } + public List Rights { get; set; } + public List MetaItems { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataContributor.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataContributor.cs new file mode 100644 index 000000000..8a8981925 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataContributor.cs @@ -0,0 +1,9 @@ +namespace VersOne.Epub.Schema +{ + public class EpubMetadataContributor + { + public string Contributor { get; set; } + public string FileAs { get; set; } + public string Role { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataCreator.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataCreator.cs new file mode 100644 index 000000000..015b84029 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataCreator.cs @@ -0,0 +1,9 @@ +namespace VersOne.Epub.Schema +{ + public class EpubMetadataCreator + { + public string Creator { get; set; } + public string FileAs { get; set; } + public string Role { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataDate.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataDate.cs new file mode 100644 index 000000000..7e882ec4d --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataDate.cs @@ -0,0 +1,8 @@ +namespace VersOne.Epub.Schema +{ + public class EpubMetadataDate + { + public string Date { get; set; } + public string Event { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataIdentifier.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataIdentifier.cs new file mode 100644 index 000000000..61d3c2fd8 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataIdentifier.cs @@ -0,0 +1,9 @@ +namespace VersOne.Epub.Schema +{ + public class EpubMetadataIdentifier + { + public string Id { get; set; } + public string Scheme { get; set; } + public string Identifier { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataMeta.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataMeta.cs new file mode 100644 index 000000000..133199de9 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubMetadataMeta.cs @@ -0,0 +1,12 @@ +namespace VersOne.Epub.Schema +{ + public class EpubMetadataMeta + { + public string Name { get; set; } + public string Content { get; set; } + public string Id { get; set; } + public string Refines { get; set; } + public string Property { get; set; } + public string Scheme { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubPackage.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubPackage.cs new file mode 100644 index 000000000..50b5b354a --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubPackage.cs @@ -0,0 +1,15 @@ +using VersOne.Epub.Internal; + +namespace VersOne.Epub.Schema +{ + public class EpubPackage + { + public EpubVersion EpubVersion { get; set; } + public EpubMetadata Metadata { get; set; } + + public string GetVersionString() + { + return VersionUtils.GetVersionString(EpubVersion); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubVersion.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubVersion.cs new file mode 100644 index 000000000..b556b5c76 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/EpubVersion.cs @@ -0,0 +1,26 @@ +using System; + +namespace VersOne.Epub.Schema +{ + public enum EpubVersion + { + [VersionString("2.0")] + EPUB_2 = 2, + + [VersionString("3.0")] + EPUB_3_0, + + [VersionString("3.1")] + EPUB_3_1 + } + + public class VersionStringAttribute : Attribute + { + public VersionStringAttribute(string version) + { + Version = version; + } + + public string Version { get; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/PageProgressionDirection.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/PageProgressionDirection.cs new file mode 100644 index 000000000..8f66cdbe0 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Schema/Opf/PageProgressionDirection.cs @@ -0,0 +1,28 @@ +namespace VersOne.Epub.Schema +{ + public enum PageProgressionDirection + { + DEFAULT = 1, + LEFT_TO_RIGHT, + RIGHT_TO_LEFT, + UNKNOWN + } + + internal static class PageProgressionDirectionParser + { + public static PageProgressionDirection Parse(string stringValue) + { + switch (stringValue.ToLowerInvariant()) + { + case "default": + return PageProgressionDirection.DEFAULT; + case "ltr": + return PageProgressionDirection.LEFT_TO_RIGHT; + case "rtl": + return PageProgressionDirection.RIGHT_TO_LEFT; + default: + return PageProgressionDirection.UNKNOWN; + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/StringExtensionMethods.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/StringExtensionMethods.cs new file mode 100644 index 000000000..a8013a2e3 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/StringExtensionMethods.cs @@ -0,0 +1,12 @@ +using System; + +namespace VersOne.Epub.Utils +{ + public static class StringExtensionMethods + { + public static bool CompareOrdinalIgnoreCase(this string source, string value) + { + return string.Compare(source, value, StringComparison.OrdinalIgnoreCase) == 0; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/VersionUtils.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/VersionUtils.cs new file mode 100644 index 000000000..4a7784bd8 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/VersionUtils.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using VersOne.Epub.Schema; + +namespace VersOne.Epub.Internal +{ + public static class VersionUtils + { + public static string GetVersionString(EpubVersion epubVersion) + { + var epubVersionType = typeof(EpubVersion); + var fieldInfo = epubVersionType.GetRuntimeField(epubVersion.ToString()); + + if (fieldInfo != null) + { + return fieldInfo.GetCustomAttribute().Version; + } + else + { + return epubVersion.ToString(); + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/XmlExtensionMethods.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/XmlExtensionMethods.cs new file mode 100644 index 000000000..c18f39d28 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/XmlExtensionMethods.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; + +namespace VersOne.Epub.Utils +{ + public static class XmlExtensionMethods + { + public static string GetLowerCaseLocalName(this XAttribute xAttribute) + { + return xAttribute.Name.LocalName.ToLowerInvariant(); + } + + public static string GetLowerCaseLocalName(this XElement xElement) + { + return xElement.Name.LocalName.ToLowerInvariant(); + } + + public static bool CompareNameTo(this XElement xElement, string value) + { + return xElement.Name.LocalName.CompareOrdinalIgnoreCase(value); + } + + public static bool CompareValueTo(this XAttribute xAttribute, string value) + { + return xAttribute.Value.CompareOrdinalIgnoreCase(value); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/XmlUtils.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/XmlUtils.cs new file mode 100644 index 000000000..abc936e41 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/XmlUtils.cs @@ -0,0 +1,58 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; + +namespace VersOne.Epub.Internal +{ + public static class XmlUtils + { + public static async Task LoadDocumentAsync(Stream stream) + { + using (var memoryStream = new MemoryStream()) + { + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + var xmlReaderSettings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Ignore, + Async = true + }; + using (var xmlReader = XmlReader.Create(memoryStream, xmlReaderSettings)) + { + return await Task.Run(() => LoadXDocument(memoryStream)).ConfigureAwait(false); + } + } + } + + private static XDocument LoadXDocument(MemoryStream memoryStream) + { + try + { + return XDocument.Load(memoryStream); + } + catch (XmlException) + { + // .NET can't handle XML 1.1, so try sanitising and reading as 1.0 + memoryStream.Position = 0; + using (var sr = new StreamReader(memoryStream)) + { + var text = sr.ReadToEnd(); + + if (text.StartsWith(@" XmlConvert.IsXmlChar(x)).ToArray(); + var sanitised = new string(chars); + + return XDocument.Parse(sanitised); + } + } + + throw; + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/ZipPathUtils.cs b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/ZipPathUtils.cs new file mode 100644 index 000000000..f5e564d6e --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpubTag/Utils/ZipPathUtils.cs @@ -0,0 +1,37 @@ +namespace VersOne.Epub.Internal +{ + public static class ZipPathUtils + { + public static string GetDirectoryPath(string filePath) + { + var lastSlashIndex = filePath.LastIndexOf('/'); + if (lastSlashIndex == -1) + { + return string.Empty; + } + else + { + return filePath.Substring(0, lastSlashIndex); + } + } + + public static string Combine(string directory, string fileName) + { + if (string.IsNullOrEmpty(directory)) + { + return fileName; + } + else + { + while (fileName.StartsWith("../")) + { + var idx = directory.LastIndexOf("/"); + directory = idx > 0 ? directory.Substring(0, idx) : string.Empty; + fileName = fileName.Substring(3); + } + + return string.IsNullOrEmpty(directory) ? fileName : string.Concat(directory, "/", fileName); + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/AlbumImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/AlbumImportedEvent.cs index d2fed1aeb..228b2448d 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/AlbumImportedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/AlbumImportedEvent.cs @@ -7,20 +7,18 @@ namespace NzbDrone.Core.MediaFiles.Events { public class AlbumImportedEvent : IEvent { - public Artist Artist { get; private set; } - public Album Album { get; private set; } - public AlbumRelease AlbumRelease { get; private set; } - public List ImportedTracks { get; private set; } - public List OldFiles { get; private set; } + public Author Artist { get; private set; } + public Book Album { get; private set; } + public List ImportedTracks { get; private set; } + public List OldFiles { get; private set; } public bool NewDownload { get; private set; } public string DownloadClient { get; private set; } public string DownloadId { get; private set; } - public AlbumImportedEvent(Artist artist, Album album, AlbumRelease release, List importedTracks, List oldFiles, bool newDownload, DownloadClientItem downloadClientItem) + public AlbumImportedEvent(Author artist, Book album, List importedTracks, List oldFiles, bool newDownload, DownloadClientItem downloadClientItem) { Artist = artist; Album = album; - AlbumRelease = release; ImportedTracks = importedTracks; OldFiles = oldFiles; NewDownload = newDownload; diff --git a/src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs index bea4ed219..65385efd1 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs @@ -5,9 +5,9 @@ namespace NzbDrone.Core.MediaFiles.Events { public class ArtistRenamedEvent : IEvent { - public Artist Artist { get; private set; } + public Author Artist { get; private set; } - public ArtistRenamedEvent(Artist artist) + public ArtistRenamedEvent(Author artist) { Artist = artist; } diff --git a/src/NzbDrone.Core/MediaFiles/Events/ArtistScanSkippedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/ArtistScanSkippedEvent.cs index 9dae29110..19d40054c 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/ArtistScanSkippedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/ArtistScanSkippedEvent.cs @@ -5,10 +5,10 @@ namespace NzbDrone.Core.MediaFiles.Events { public class ArtistScanSkippedEvent : IEvent { - public Artist Artist { get; private set; } + public Author Artist { get; private set; } public ArtistScanSkippedReason Reason { get; private set; } - public ArtistScanSkippedEvent(Artist artist, ArtistScanSkippedReason reason) + public ArtistScanSkippedEvent(Author artist, ArtistScanSkippedReason reason) { Artist = artist; Reason = reason; diff --git a/src/NzbDrone.Core/MediaFiles/Events/ArtistScannedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/ArtistScannedEvent.cs index d798cd0fd..9acd144e0 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/ArtistScannedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/ArtistScannedEvent.cs @@ -5,9 +5,9 @@ namespace NzbDrone.Core.MediaFiles.Events { public class ArtistScannedEvent : IEvent { - public Artist Artist { get; private set; } + public Author Artist { get; private set; } - public ArtistScannedEvent(Artist artist) + public ArtistScannedEvent(Author artist) { Artist = artist; } diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackFileAddedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackFileAddedEvent.cs index 236365ac9..975c0d142 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/TrackFileAddedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackFileAddedEvent.cs @@ -4,9 +4,9 @@ namespace NzbDrone.Core.MediaFiles.Events { public class TrackFileAddedEvent : IEvent { - public TrackFile TrackFile { get; private set; } + public BookFile TrackFile { get; private set; } - public TrackFileAddedEvent(TrackFile trackFile) + public TrackFileAddedEvent(BookFile trackFile) { TrackFile = trackFile; } diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs index d28c7205d..ae94d2841 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs @@ -4,10 +4,10 @@ namespace NzbDrone.Core.MediaFiles.Events { public class TrackFileDeletedEvent : IEvent { - public TrackFile TrackFile { get; private set; } + public BookFile TrackFile { get; private set; } public DeleteMediaFileReason Reason { get; private set; } - public TrackFileDeletedEvent(TrackFile trackFile, DeleteMediaFileReason reason) + public TrackFileDeletedEvent(BookFile trackFile, DeleteMediaFileReason reason) { TrackFile = trackFile; Reason = reason; diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackFileRenamedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackFileRenamedEvent.cs index a4c1847fc..6119699e3 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/TrackFileRenamedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackFileRenamedEvent.cs @@ -5,11 +5,11 @@ namespace NzbDrone.Core.MediaFiles.Events { public class TrackFileRenamedEvent : IEvent { - public Artist Artist { get; private set; } - public TrackFile TrackFile { get; private set; } + public Author Artist { get; private set; } + public BookFile TrackFile { get; private set; } public string OriginalPath { get; private set; } - public TrackFileRenamedEvent(Artist artist, TrackFile trackFile, string originalPath) + public TrackFileRenamedEvent(Author artist, BookFile trackFile, string originalPath) { Artist = artist; TrackFile = trackFile; diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackFileRetaggedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackFileRetaggedEvent.cs index 3023103ec..6917eabb5 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/TrackFileRetaggedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackFileRetaggedEvent.cs @@ -7,13 +7,13 @@ namespace NzbDrone.Core.MediaFiles.Events { public class TrackFileRetaggedEvent : IEvent { - public Artist Artist { get; private set; } - public TrackFile TrackFile { get; private set; } + public Author Artist { get; private set; } + public BookFile TrackFile { get; private set; } public Dictionary> Diff { get; private set; } public bool Scrubbed { get; private set; } - public TrackFileRetaggedEvent(Artist artist, - TrackFile trackFile, + public TrackFileRetaggedEvent(Author artist, + BookFile trackFile, Dictionary> diff, bool scrubbed) { diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackFolderCreatedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackFolderCreatedEvent.cs index 79fb40666..7d715cbca 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/TrackFolderCreatedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackFolderCreatedEvent.cs @@ -5,13 +5,13 @@ namespace NzbDrone.Core.MediaFiles.Events { public class TrackFolderCreatedEvent : IEvent { - public Artist Artist { get; private set; } - public TrackFile TrackFile { get; private set; } + public Author Artist { get; private set; } + public BookFile TrackFile { get; private set; } public string ArtistFolder { get; set; } public string AlbumFolder { get; set; } public string TrackFolder { get; set; } - public TrackFolderCreatedEvent(Artist artist, TrackFile trackFile) + public TrackFolderCreatedEvent(Author artist, BookFile trackFile) { Artist = artist; TrackFile = trackFile; diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs index 7139fd7fd..3957e80b5 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs @@ -8,13 +8,13 @@ namespace NzbDrone.Core.MediaFiles.Events public class TrackImportedEvent : IEvent { public LocalTrack TrackInfo { get; private set; } - public TrackFile ImportedTrack { get; private set; } - public List OldFiles { get; private set; } + public BookFile ImportedTrack { get; private set; } + public List OldFiles { get; private set; } public bool NewDownload { get; private set; } public string DownloadClient { get; private set; } public string DownloadId { get; private set; } - public TrackImportedEvent(LocalTrack trackInfo, TrackFile importedTrack, List oldFiles, bool newDownload, DownloadClientItem downloadClientItem) + public TrackImportedEvent(LocalTrack trackInfo, BookFile importedTrack, List oldFiles, bool newDownload, DownloadClientItem downloadClientItem) { TrackInfo = trackInfo; ImportedTrack = importedTrack; diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs index 238f3c7e2..11c7c3471 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs @@ -16,8 +16,8 @@ namespace NzbDrone.Core.MediaFiles { public interface IDeleteMediaFiles { - void DeleteTrackFile(Artist artist, TrackFile trackFile); - void DeleteTrackFile(TrackFile trackFile, string subfolder = ""); + void DeleteTrackFile(Author artist, BookFile trackFile); + void DeleteTrackFile(BookFile trackFile, string subfolder = ""); } public class MediaFileDeletionService : IDeleteMediaFiles, @@ -47,7 +47,7 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } - public void DeleteTrackFile(Artist artist, TrackFile trackFile) + public void DeleteTrackFile(Author artist, BookFile trackFile) { var fullPath = trackFile.Path; var rootFolder = _diskProvider.GetParentFolder(artist.Path); @@ -76,7 +76,7 @@ namespace NzbDrone.Core.MediaFiles } } - public void DeleteTrackFile(TrackFile trackFile, string subfolder = "") + public void DeleteTrackFile(BookFile trackFile, string subfolder = "") { var fullPath = trackFile.Path; diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs index 8d749a98a..c7064a4fe 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs @@ -1,40 +1,44 @@ using System; using System.Collections.Generic; +using System.Linq; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.MediaFiles { public static class MediaFileExtensions { - private static Dictionary _fileExtensions; + private static readonly Dictionary _textExtensions; + private static readonly Dictionary _audioExtensions; static MediaFileExtensions() { - _fileExtensions = new Dictionary(StringComparer.OrdinalIgnoreCase) + _textExtensions = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { ".epub", Quality.EPUB }, + { ".mobi", Quality.MOBI }, + { ".azw3", Quality.AZW3 }, + { ".pdf", Quality.PDF }, + }; + + _audioExtensions = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { ".mp2", Quality.Unknown }, - { ".mp3", Quality.Unknown }, - { ".m4a", Quality.Unknown }, - { ".m4b", Quality.Unknown }, - { ".m4p", Quality.Unknown }, - { ".ogg", Quality.Unknown }, - { ".oga", Quality.Unknown }, - { ".opus", Quality.Unknown }, - { ".wma", Quality.WMA }, - { ".wav", Quality.WAV }, - { ".wv", Quality.WAVPACK }, - { ".flac", Quality.FLAC }, - { ".ape", Quality.APE } }; } - public static HashSet Extensions => new HashSet(_fileExtensions.Keys, StringComparer.OrdinalIgnoreCase); + public static HashSet TextExtensions => new HashSet(_textExtensions.Keys, StringComparer.OrdinalIgnoreCase); + public static HashSet AudioExtensions => new HashSet(_audioExtensions.Keys, StringComparer.OrdinalIgnoreCase); + public static HashSet AllExtensions => new HashSet(_textExtensions.Keys.Concat(_audioExtensions.Keys), StringComparer.OrdinalIgnoreCase); public static Quality GetQualityForExtension(string extension) { - if (_fileExtensions.ContainsKey(extension)) + if (_textExtensions.ContainsKey(extension)) + { + return _textExtensions[extension]; + } + + if (_audioExtensions.ContainsKey(extension)) { - return _fileExtensions[extension]; + return _audioExtensions[extension]; } return Quality.Unknown; diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 5148be7c4..1b3dbc8fc 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -9,20 +9,19 @@ using NzbDrone.Core.Music; namespace NzbDrone.Core.MediaFiles { - public interface IMediaFileRepository : IBasicRepository + public interface IMediaFileRepository : IBasicRepository { - List GetFilesByArtist(int artistId); - List GetFilesByAlbum(int albumId); - List GetFilesByRelease(int releaseId); - List GetUnmappedFiles(); - List GetFilesWithBasePath(string path); - List GetFileWithPath(List paths); - TrackFile GetFileWithPath(string path); - void DeleteFilesByAlbum(int albumId); - void UnlinkFilesByAlbum(int albumId); + List GetFilesByArtist(int authorId); + List GetFilesByAlbum(int bookId); + List GetUnmappedFiles(); + List GetFilesWithBasePath(string path); + List GetFileWithPath(List paths); + BookFile GetFileWithPath(string path); + void DeleteFilesByAlbum(int bookId); + void UnlinkFilesByAlbum(int bookId); } - public class MediaFileRepository : BasicRepository, IMediaFileRepository + public class MediaFileRepository : BasicRepository, IMediaFileRepository { public MediaFileRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) @@ -32,128 +31,91 @@ namespace NzbDrone.Core.MediaFiles // always join with all the other good stuff // needed more often than not so better to load it all now protected override SqlBuilder Builder() => new SqlBuilder() - .LeftJoin((t, x) => t.Id == x.TrackFileId) - .LeftJoin((t, a) => t.AlbumId == a.Id) - .LeftJoin((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) - .LeftJoin((a, m) => a.ArtistMetadataId == m.Id); + .LeftJoin((t, a) => t.BookId == a.Id) + .LeftJoin((album, artist) => album.AuthorMetadataId == artist.AuthorMetadataId) + .LeftJoin((a, m) => a.AuthorMetadataId == m.Id); - protected override List Query(SqlBuilder builder) => Query(_database, builder).ToList(); + protected override List Query(SqlBuilder builder) => Query(_database, builder).ToList(); - public static IEnumerable Query(IDatabase database, SqlBuilder builder) + public static IEnumerable Query(IDatabase database, SqlBuilder builder) { - var fileDictionary = new Dictionary(); - - _ = database.QueryJoined(builder, (file, track, album, artist, metadata) => Map(fileDictionary, file, track, album, artist, metadata)); - - return fileDictionary.Values; + return database.QueryJoined(builder, (file, album, artist, metadata) => Map(file, album, artist, metadata)); } - private static TrackFile Map(Dictionary dict, TrackFile file, Track track, Album album, Artist artist, ArtistMetadata metadata) + private static BookFile Map(BookFile file, Book album, Author artist, AuthorMetadata metadata) { - if (!dict.TryGetValue(file.Id, out var entry)) - { - if (artist != null) - { - artist.Metadata = metadata; - } - - entry = file; - entry.Tracks = new List(); - entry.Album = album; - entry.Artist = artist; - dict.Add(entry.Id, entry); - } + file.Album = album; - if (track != null) + if (artist != null) { - entry.Tracks.Value.Add(track); + artist.Metadata = metadata; } - return entry; + file.Artist = artist; + + return file; } - public List GetFilesByArtist(int artistId) + public List GetFilesByArtist(int authorId) { - return Query(Builder().LeftJoin((t, r) => t.AlbumReleaseId == r.Id) - .Where(r => r.Monitored == true) - .Where(a => a.Id == artistId)); + return Query(Builder().Where(a => a.Id == authorId)); } - public List GetFilesByAlbum(int albumId) + public List GetFilesByAlbum(int bookId) { - return Query(Builder().LeftJoin((t, r) => t.AlbumReleaseId == r.Id) - .Where(r => r.Monitored == true) - .Where(f => f.AlbumId == albumId)); + return Query(Builder().Where(f => f.BookId == bookId)); } - public List GetUnmappedFiles() + public List GetUnmappedFiles() { //x.Id == null is converted to SQL, so warning incorrect #pragma warning disable CS0472 - return _database.Query(new SqlBuilder().Select(typeof(TrackFile)) - .LeftJoin((f, t) => f.Id == t.TrackFileId) - .Where(t => t.Id == null)).ToList(); + return _database.Query(new SqlBuilder().Select(typeof(BookFile)) + .LeftJoin((f, t) => f.BookId == t.Id) + .Where(t => t.Id == null)).ToList(); #pragma warning restore CS0472 } - public void DeleteFilesByAlbum(int albumId) - { - Delete(x => x.AlbumId == albumId); - } - - public void UnlinkFilesByAlbum(int albumId) + public void DeleteFilesByAlbum(int bookId) { - var files = Query(x => x.AlbumId == albumId); - files.ForEach(x => x.AlbumId = 0); - SetFields(files, f => f.AlbumId); + Delete(x => x.BookId == bookId); } - public List GetFilesByRelease(int releaseId) + public void UnlinkFilesByAlbum(int bookId) { - return Query(Builder().Where(x => x.AlbumReleaseId == releaseId)); + var files = Query(x => x.BookId == bookId); + files.ForEach(x => x.BookId = 0); + SetFields(files, f => f.BookId); } - public List GetFilesWithBasePath(string path) + public List GetFilesWithBasePath(string path) { // ensure path ends with a single trailing path separator to avoid matching partial paths var safePath = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; - return _database.Query(new SqlBuilder().Where(x => x.Path.StartsWith(safePath))).ToList(); + return _database.Query(new SqlBuilder().Where(x => x.Path.StartsWith(safePath))).ToList(); } - public TrackFile GetFileWithPath(string path) + public BookFile GetFileWithPath(string path) { return Query(x => x.Path == path).SingleOrDefault(); } - public List GetFileWithPath(List paths) + public List GetFileWithPath(List paths) { // use more limited join for speed var builder = new SqlBuilder() - .LeftJoin((f, t) => f.Id == t.TrackFileId); + .LeftJoin((f, t) => f.BookId == t.Id); - var dict = new Dictionary(); - _ = _database.QueryJoined(builder, (file, track) => MapTrack(dict, file, track)).ToList(); - var all = dict.Values.ToList(); + var all = _database.QueryJoined(builder, (file, book) => MapTrack(file, book)).ToList(); var joined = all.Join(paths, x => x.Path, x => x, (file, path) => file, PathEqualityComparer.Instance).ToList(); return joined; } - private TrackFile MapTrack(Dictionary dict, TrackFile file, Track track) + private BookFile MapTrack(BookFile file, Book book) { - if (!dict.TryGetValue(file.Id, out var entry)) - { - entry = file; - entry.Tracks = new List(); - dict.Add(entry.Id, entry); - } - - if (track != null) - { - entry.Tracks.Value.Add(track); - } - - return entry; + file.Album = book; + return file; } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index e97b78e47..aab47538e 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -15,23 +15,22 @@ namespace NzbDrone.Core.MediaFiles { public interface IMediaFileService { - TrackFile Add(TrackFile trackFile); - void AddMany(List trackFiles); - void Update(TrackFile trackFile); - void Update(List trackFile); - void Delete(TrackFile trackFile, DeleteMediaFileReason reason); - void DeleteMany(List trackFiles, DeleteMediaFileReason reason); - List GetFilesByArtist(int artistId); - List GetFilesByAlbum(int albumId); - List GetFilesByRelease(int releaseId); - List GetUnmappedFiles(); + BookFile Add(BookFile trackFile); + void AddMany(List trackFiles); + void Update(BookFile trackFile); + void Update(List trackFile); + void Delete(BookFile trackFile, DeleteMediaFileReason reason); + void DeleteMany(List trackFiles, DeleteMediaFileReason reason); + List GetFilesByArtist(int authorId); + List GetFilesByAlbum(int bookId); + List GetUnmappedFiles(); List FilterUnchangedFiles(List files, FilterFilesType filter); - TrackFile Get(int id); - List Get(IEnumerable ids); - List GetFilesWithBasePath(string path); - List GetFileWithPath(List path); - TrackFile GetFileWithPath(string path); - void UpdateMediaInfo(List trackFiles); + BookFile Get(int id); + List Get(IEnumerable ids); + List GetFilesWithBasePath(string path); + List GetFileWithPath(List path); + BookFile GetFileWithPath(string path); + void UpdateMediaInfo(List trackFiles); } public class MediaFileService : IMediaFileService, @@ -50,14 +49,14 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } - public TrackFile Add(TrackFile trackFile) + public BookFile Add(BookFile trackFile) { var addedFile = _mediaFileRepository.Insert(trackFile); _eventAggregator.PublishEvent(new TrackFileAddedEvent(addedFile)); return addedFile; } - public void AddMany(List trackFiles) + public void AddMany(List trackFiles) { _mediaFileRepository.InsertMany(trackFiles); foreach (var addedFile in trackFiles) @@ -66,33 +65,33 @@ namespace NzbDrone.Core.MediaFiles } } - public void Update(TrackFile trackFile) + public void Update(BookFile trackFile) { _mediaFileRepository.Update(trackFile); } - public void Update(List trackFiles) + public void Update(List trackFiles) { _mediaFileRepository.UpdateMany(trackFiles); } - public void Delete(TrackFile trackFile, DeleteMediaFileReason reason) + public void Delete(BookFile trackFile, DeleteMediaFileReason reason) { _mediaFileRepository.Delete(trackFile); // If the trackfile wasn't mapped to a track, don't publish an event - if (trackFile.AlbumId > 0) + if (trackFile.BookId > 0) { _eventAggregator.PublishEvent(new TrackFileDeletedEvent(trackFile, reason)); } } - public void DeleteMany(List trackFiles, DeleteMediaFileReason reason) + public void DeleteMany(List trackFiles, DeleteMediaFileReason reason) { _mediaFileRepository.DeleteMany(trackFiles); // publish events where trackfile was mapped to a track - foreach (var trackFile in trackFiles.Where(x => x.AlbumId > 0)) + foreach (var trackFile in trackFiles.Where(x => x.BookId > 0)) { _eventAggregator.PublishEvent(new TrackFileDeletedEvent(trackFile, reason)); } @@ -139,7 +138,7 @@ namespace NzbDrone.Core.MediaFiles unwanted = combined .Where(x => x.DiskFile.Length == x.DbFile.Size && Math.Abs((x.DiskFile.LastWriteTimeUtc - x.DbFile.Modified).TotalSeconds) <= 1 && - (x.DbFile.Tracks == null || (x.DbFile.Tracks.IsLoaded && x.DbFile.Tracks.Value.Any()))) + (x.DbFile.Album == null || (x.DbFile.Album.IsLoaded && x.DbFile.Album.Value != null))) .Select(x => x.DiskFile) .ToList(); _logger.Trace($"{unwanted.Count} unchanged and matched files"); @@ -152,52 +151,47 @@ namespace NzbDrone.Core.MediaFiles return files.Except(unwanted).ToList(); } - public TrackFile Get(int id) + public BookFile Get(int id) { return _mediaFileRepository.Get(id); } - public List Get(IEnumerable ids) + public List Get(IEnumerable ids) { return _mediaFileRepository.Get(ids).ToList(); } - public List GetFilesWithBasePath(string path) + public List GetFilesWithBasePath(string path) { return _mediaFileRepository.GetFilesWithBasePath(path); } - public List GetFileWithPath(List path) + public List GetFileWithPath(List path) { return _mediaFileRepository.GetFileWithPath(path); } - public TrackFile GetFileWithPath(string path) + public BookFile GetFileWithPath(string path) { return _mediaFileRepository.GetFileWithPath(path); } - public List GetFilesByArtist(int artistId) + public List GetFilesByArtist(int authorId) { - return _mediaFileRepository.GetFilesByArtist(artistId); + return _mediaFileRepository.GetFilesByArtist(authorId); } - public List GetFilesByAlbum(int albumId) + public List GetFilesByAlbum(int bookId) { - return _mediaFileRepository.GetFilesByAlbum(albumId); + return _mediaFileRepository.GetFilesByAlbum(bookId); } - public List GetFilesByRelease(int releaseId) - { - return _mediaFileRepository.GetFilesByRelease(releaseId); - } - - public List GetUnmappedFiles() + public List GetUnmappedFiles() { return _mediaFileRepository.GetUnmappedFiles(); } - public void UpdateMediaInfo(List trackFiles) + public void UpdateMediaInfo(List trackFiles) { _mediaFileRepository.SetFields(trackFiles, t => t.MediaInfo); } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index 0e4e5fcf6..350354bdc 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -3,7 +3,6 @@ using System.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Music; namespace NzbDrone.Core.MediaFiles { @@ -15,15 +14,12 @@ namespace NzbDrone.Core.MediaFiles public class MediaFileTableCleanupService : IMediaFileTableCleanupService { private readonly IMediaFileService _mediaFileService; - private readonly ITrackService _trackService; private readonly Logger _logger; public MediaFileTableCleanupService(IMediaFileService mediaFileService, - ITrackService trackService, Logger logger) { _mediaFileService = mediaFileService; - _trackService = trackService; _logger = logger; } @@ -38,11 +34,6 @@ namespace NzbDrone.Core.MediaFiles string.Join("\n", missingFiles.Select(x => x.Path))); _mediaFileService.DeleteMany(missingFiles, DeleteMediaFileReason.MissingFromDisk); - - // get any tracks matched to these trackfiles and unlink them - var orphanedTracks = _trackService.GetTracksByFileId(missingFiles.Select(x => x.Id)); - orphanedTracks.ForEach(x => x.TrackFileId = 0); - _trackService.SetFileIds(orphanedTracks); } } } diff --git a/src/NzbDrone.Core/MediaFiles/RenameTrackFilePreview.cs b/src/NzbDrone.Core/MediaFiles/RenameTrackFilePreview.cs index 25bfe75f7..a85c0dd10 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameTrackFilePreview.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameTrackFilePreview.cs @@ -4,8 +4,8 @@ namespace NzbDrone.Core.MediaFiles { public class RenameTrackFilePreview { - public int ArtistId { get; set; } - public int AlbumId { get; set; } + public int AuthorId { get; set; } + public int BookId { get; set; } public List TrackNumbers { get; set; } public int TrackFileId { get; set; } public string ExistingPath { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs index 0944780e6..32176f285 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs @@ -17,91 +17,86 @@ namespace NzbDrone.Core.MediaFiles { public interface IRenameTrackFileService { - List GetRenamePreviews(int artistId); - List GetRenamePreviews(int artistId, int albumId); + List GetRenamePreviews(int authorId); + List GetRenamePreviews(int authorId, int bookId); } public class RenameTrackFileService : IRenameTrackFileService, IExecute, IExecute { private readonly IArtistService _artistService; - private readonly IAlbumService _albumService; private readonly IMediaFileService _mediaFileService; private readonly IMoveTrackFiles _trackFileMover; private readonly IEventAggregator _eventAggregator; - private readonly ITrackService _trackService; private readonly IBuildFileNames _filenameBuilder; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; public RenameTrackFileService(IArtistService artistService, - IAlbumService albumService, IMediaFileService mediaFileService, IMoveTrackFiles trackFileMover, IEventAggregator eventAggregator, - ITrackService trackService, IBuildFileNames filenameBuilder, IDiskProvider diskProvider, Logger logger) { _artistService = artistService; - _albumService = albumService; _mediaFileService = mediaFileService; _trackFileMover = trackFileMover; _eventAggregator = eventAggregator; - _trackService = trackService; _filenameBuilder = filenameBuilder; _diskProvider = diskProvider; _logger = logger; } - public List GetRenamePreviews(int artistId) + public List GetRenamePreviews(int authorId) { - var artist = _artistService.GetArtist(artistId); - var tracks = _trackService.GetTracksByArtist(artistId); - var files = _mediaFileService.GetFilesByArtist(artistId); + var artist = _artistService.GetArtist(authorId); + var files = _mediaFileService.GetFilesByArtist(authorId); - return GetPreviews(artist, tracks, files) - .OrderByDescending(e => e.AlbumId) - .ThenByDescending(e => e.TrackNumbers.First()) + _logger.Trace($"got {files.Count} files"); + + return GetPreviews(artist, files) + .OrderByDescending(e => e.BookId) .ToList(); } - public List GetRenamePreviews(int artistId, int albumId) + public List GetRenamePreviews(int authorId, int bookId) { - var artist = _artistService.GetArtist(artistId); - var tracks = _trackService.GetTracksByAlbum(albumId); - var files = _mediaFileService.GetFilesByAlbum(albumId); + var artist = _artistService.GetArtist(authorId); + var files = _mediaFileService.GetFilesByAlbum(bookId); - return GetPreviews(artist, tracks, files) + return GetPreviews(artist, files) .OrderByDescending(e => e.TrackNumbers.First()).ToList(); } - private IEnumerable GetPreviews(Artist artist, List tracks, List files) + private IEnumerable GetPreviews(Author artist, List files) { foreach (var f in files) { var file = f; - var tracksInFile = tracks.Where(e => e.TrackFileId == file.Id).ToList(); + var book = file.Album.Value; var trackFilePath = file.Path; - if (!tracksInFile.Any()) + if (book == null) { - _logger.Warn("File ({0}) is not linked to any tracks", trackFilePath); + _logger.Warn("File ({0}) is not linked to a book", trackFilePath); continue; } - var album = _albumService.GetAlbum(tracksInFile.First().AlbumId); + var newName = _filenameBuilder.BuildTrackFileName(artist, book, file); + + _logger.Trace($"got name {newName}"); + + var newPath = _filenameBuilder.BuildTrackFilePath(artist, book, newName, Path.GetExtension(trackFilePath)); - var newName = _filenameBuilder.BuildTrackFileName(tracksInFile, artist, album, file); - var newPath = _filenameBuilder.BuildTrackFilePath(artist, album, newName, Path.GetExtension(trackFilePath)); + _logger.Trace($"got path {newPath}"); if (!trackFilePath.PathEquals(newPath, StringComparison.Ordinal)) { yield return new RenameTrackFilePreview { - ArtistId = artist.Id, - AlbumId = album.Id, - TrackNumbers = tracksInFile.Select(e => e.AbsoluteTrackNumber).ToList(), + AuthorId = artist.Id, + BookId = book.Id, TrackFileId = file.Id, ExistingPath = file.Path, NewPath = newPath @@ -110,9 +105,9 @@ namespace NzbDrone.Core.MediaFiles } } - private void RenameFiles(List trackFiles, Artist artist) + private void RenameFiles(List trackFiles, Author artist) { - var renamed = new List(); + var renamed = new List(); foreach (var trackFile in trackFiles) { @@ -151,7 +146,7 @@ namespace NzbDrone.Core.MediaFiles public void Execute(RenameFilesCommand message) { - var artist = _artistService.GetArtist(message.ArtistId); + var artist = _artistService.GetArtist(message.AuthorId); var trackFiles = _mediaFileService.Get(message.Files); _logger.ProgressInfo("Renaming {0} files for {1}", trackFiles.Count, artist.Name); @@ -162,7 +157,7 @@ namespace NzbDrone.Core.MediaFiles public void Execute(RenameArtistCommand message) { _logger.Debug("Renaming all files for selected artist"); - var artistToRename = _artistService.GetArtists(message.ArtistIds); + var artistToRename = _artistService.GetArtists(message.AuthorIds); foreach (var artist in artistToRename) { diff --git a/src/NzbDrone.Core/MediaFiles/RetagTrackFilePreview.cs b/src/NzbDrone.Core/MediaFiles/RetagTrackFilePreview.cs index 97e34aed6..0149cc1ba 100644 --- a/src/NzbDrone.Core/MediaFiles/RetagTrackFilePreview.cs +++ b/src/NzbDrone.Core/MediaFiles/RetagTrackFilePreview.cs @@ -5,8 +5,8 @@ namespace NzbDrone.Core.MediaFiles { public class RetagTrackFilePreview { - public int ArtistId { get; set; } - public int AlbumId { get; set; } + public int AuthorId { get; set; } + public int BookId { get; set; } public List TrackNumbers { get; set; } public int TrackFileId { get; set; } public string Path { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/RootFolderWatchingService.cs b/src/NzbDrone.Core/MediaFiles/RootFolderWatchingService.cs index 9ce640b82..5e088211f 100644 --- a/src/NzbDrone.Core/MediaFiles/RootFolderWatchingService.cs +++ b/src/NzbDrone.Core/MediaFiles/RootFolderWatchingService.cs @@ -257,7 +257,7 @@ namespace NzbDrone.Core.MediaFiles return true; } - if (extension.IsNotNullOrWhiteSpace() && !MediaFileExtensions.Extensions.Contains(extension)) + if (extension.IsNotNullOrWhiteSpace() && !MediaFileExtensions.AllExtensions.Contains(extension)) { return true; } diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMoveResult.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMoveResult.cs index b115e3b65..9e98c059f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFileMoveResult.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMoveResult.cs @@ -6,10 +6,10 @@ namespace NzbDrone.Core.MediaFiles { public TrackFileMoveResult() { - OldFiles = new List(); + OldFiles = new List(); } - public TrackFile TrackFile { get; set; } - public List OldFiles { get; set; } + public BookFile TrackFile { get; set; } + public List OldFiles { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs index 12947516f..a247cceb2 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs @@ -17,14 +17,13 @@ namespace NzbDrone.Core.MediaFiles { public interface IMoveTrackFiles { - TrackFile MoveTrackFile(TrackFile trackFile, Artist artist); - TrackFile MoveTrackFile(TrackFile trackFile, LocalTrack localTrack); - TrackFile CopyTrackFile(TrackFile trackFile, LocalTrack localTrack); + BookFile MoveTrackFile(BookFile trackFile, Author artist); + BookFile MoveTrackFile(BookFile trackFile, LocalTrack localTrack); + BookFile CopyTrackFile(BookFile trackFile, LocalTrack localTrack); } public class TrackFileMovingService : IMoveTrackFiles { - private readonly ITrackService _trackService; private readonly IAlbumService _albumService; private readonly IUpdateTrackFileService _updateTrackFileService; private readonly IBuildFileNames _buildFileNames; @@ -36,8 +35,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IConfigService _configService; private readonly Logger _logger; - public TrackFileMovingService(ITrackService trackService, - IAlbumService albumService, + public TrackFileMovingService(IAlbumService albumService, IUpdateTrackFileService updateTrackFileService, IBuildFileNames buildFileNames, IDiskTransferService diskTransferService, @@ -48,7 +46,6 @@ namespace NzbDrone.Core.MediaFiles IConfigService configService, Logger logger) { - _trackService = trackService; _albumService = albumService; _updateTrackFileService = updateTrackFileService; _buildFileNames = buildFileNames; @@ -61,35 +58,34 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } - public TrackFile MoveTrackFile(TrackFile trackFile, Artist artist) + public BookFile MoveTrackFile(BookFile trackFile, Author artist) { - var tracks = _trackService.GetTracksByFileId(trackFile.Id); - var album = _albumService.GetAlbum(trackFile.AlbumId); - var newFileName = _buildFileNames.BuildTrackFileName(tracks, artist, album, trackFile); + var album = _albumService.GetAlbum(trackFile.BookId); + var newFileName = _buildFileNames.BuildTrackFileName(artist, album, trackFile); var filePath = _buildFileNames.BuildTrackFilePath(artist, album, newFileName, Path.GetExtension(trackFile.Path)); EnsureTrackFolder(trackFile, artist, album, filePath); _logger.Debug("Renaming track file: {0} to {1}", trackFile, filePath); - return TransferFile(trackFile, artist, tracks, filePath, TransferMode.Move); + return TransferFile(trackFile, artist, null, filePath, TransferMode.Move); } - public TrackFile MoveTrackFile(TrackFile trackFile, LocalTrack localTrack) + public BookFile MoveTrackFile(BookFile trackFile, LocalTrack localTrack) { - var newFileName = _buildFileNames.BuildTrackFileName(localTrack.Tracks, localTrack.Artist, localTrack.Album, trackFile); + var newFileName = _buildFileNames.BuildTrackFileName(localTrack.Artist, localTrack.Album, trackFile); var filePath = _buildFileNames.BuildTrackFilePath(localTrack.Artist, localTrack.Album, newFileName, Path.GetExtension(localTrack.Path)); EnsureTrackFolder(trackFile, localTrack, filePath); _logger.Debug("Moving track file: {0} to {1}", trackFile.Path, filePath); - return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Move); + return TransferFile(trackFile, localTrack.Artist, null, filePath, TransferMode.Move); } - public TrackFile CopyTrackFile(TrackFile trackFile, LocalTrack localTrack) + public BookFile CopyTrackFile(BookFile trackFile, LocalTrack localTrack) { - var newFileName = _buildFileNames.BuildTrackFileName(localTrack.Tracks, localTrack.Artist, localTrack.Album, trackFile); + var newFileName = _buildFileNames.BuildTrackFileName(localTrack.Artist, localTrack.Album, trackFile); var filePath = _buildFileNames.BuildTrackFilePath(localTrack.Artist, localTrack.Album, newFileName, Path.GetExtension(localTrack.Path)); EnsureTrackFolder(trackFile, localTrack, filePath); @@ -97,14 +93,14 @@ namespace NzbDrone.Core.MediaFiles if (_configService.CopyUsingHardlinks) { _logger.Debug("Hardlinking track file: {0} to {1}", trackFile.Path, filePath); - return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.HardLinkOrCopy); + return TransferFile(trackFile, localTrack.Artist, localTrack.Album, filePath, TransferMode.HardLinkOrCopy); } _logger.Debug("Copying track file: {0} to {1}", trackFile.Path, filePath); - return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Copy); + return TransferFile(trackFile, localTrack.Artist, localTrack.Album, filePath, TransferMode.Copy); } - private TrackFile TransferFile(TrackFile trackFile, Artist artist, List tracks, string destinationFilePath, TransferMode mode) + private BookFile TransferFile(BookFile trackFile, Author artist, Book book, string destinationFilePath, TransferMode mode) { Ensure.That(trackFile, () => trackFile).IsNotNull(); Ensure.That(artist, () => artist).IsNotNull(); @@ -127,18 +123,11 @@ namespace NzbDrone.Core.MediaFiles trackFile.Path = destinationFilePath; - _updateTrackFileService.ChangeFileDateForFile(trackFile, artist, tracks); + _updateTrackFileService.ChangeFileDateForFile(trackFile, artist, book); try { _mediaFileAttributeService.SetFolderLastWriteTime(artist.Path, trackFile.DateAdded); - - if (artist.AlbumFolder) - { - var albumFolder = Path.GetDirectoryName(destinationFilePath); - - _mediaFileAttributeService.SetFolderLastWriteTime(albumFolder, trackFile.DateAdded); - } } catch (Exception ex) { @@ -150,12 +139,12 @@ namespace NzbDrone.Core.MediaFiles return trackFile; } - private void EnsureTrackFolder(TrackFile trackFile, LocalTrack localTrack, string filePath) + private void EnsureTrackFolder(BookFile trackFile, LocalTrack localTrack, string filePath) { EnsureTrackFolder(trackFile, localTrack.Artist, localTrack.Album, filePath); } - private void EnsureTrackFolder(TrackFile trackFile, Artist artist, Album album, string filePath) + private void EnsureTrackFolder(BookFile trackFile, Author artist, Book album, string filePath) { var trackFolder = Path.GetDirectoryName(filePath); var albumFolder = _buildFileNames.BuildAlbumPath(artist, album); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/AggregationService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/AggregationService.cs index b9674bf9a..2021b7fcb 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/AggregationService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/AggregationService.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Aggregation localTrack.FolderTrackInfo == null && localTrack.FileTrackInfo == null) { - if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(localTrack.Path))) + if (MediaFileExtensions.AllExtensions.Contains(Path.GetExtension(localTrack.Path))) { throw new AugmentingFailedException("Unable to parse track info from path: {0}", localTrack.Path); } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateCalibreData.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateCalibreData.cs new file mode 100644 index 000000000..0d142cb5b --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateCalibreData.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Books.Calibre; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.TrackImport.Aggregation.Aggregators +{ + public class AggregateCalibreData : IAggregate + { + private readonly Logger _logger; + private readonly ICached _bookCache; + + public AggregateCalibreData(Logger logger, + ICacheManager cacheManager) + { + _logger = logger; + _bookCache = cacheManager.GetCache(typeof(CalibreProxy)); + + _logger.Trace("Started calibre aug"); + } + + public LocalTrack Aggregate(LocalTrack localTrack, bool others) + { + var book = _bookCache.Find(localTrack.Path); + _logger.Trace($"Searching calibre data for {localTrack.Path}"); + + if (book != null) + { + _logger.Trace($"Using calibre data for {localTrack.Path}:\n{book.ToJson()}"); + + var parsed = localTrack.FileTrackInfo; + parsed.Asin = book.Identifiers.GetValueOrDefault("mobi-asin"); + parsed.Isbn = book.Identifiers.GetValueOrDefault("isbn"); + parsed.GoodreadsId = book.Identifiers.GetValueOrDefault("goodreads"); + parsed.ArtistTitle = book.AuthorSort; + parsed.AlbumTitle = book.Title; + } + + return localTrack; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateAlbumRelease.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateAlbumRelease.cs index 267d9b5cc..f05551bf8 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateAlbumRelease.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateAlbumRelease.cs @@ -9,13 +9,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification { } - public CandidateAlbumRelease(AlbumRelease release) + public CandidateAlbumRelease(Book book) { - AlbumRelease = release; - ExistingTracks = new List(); + Book = book; + ExistingTracks = new List(); } - public AlbumRelease AlbumRelease { get; set; } - public List ExistingTracks { get; set; } + public Book Book { get; set; } + public List ExistingTracks { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs index 6fb62bff0..785032065 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -13,30 +12,26 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification public interface ICandidateService { List GetDbCandidatesFromTags(LocalAlbumRelease localAlbumRelease, IdentificationOverrides idOverrides, bool includeExisting); - List GetDbCandidatesFromFingerprint(LocalAlbumRelease localAlbumRelease, IdentificationOverrides idOverrides, bool includeExisting); List GetRemoteCandidates(LocalAlbumRelease localAlbumRelease); } public class CandidateService : ICandidateService { - private readonly ISearchForNewAlbum _albumSearchService; + private readonly ISearchForNewBook _albumSearchService; private readonly IArtistService _artistService; private readonly IAlbumService _albumService; - private readonly IReleaseService _releaseService; private readonly IMediaFileService _mediaFileService; private readonly Logger _logger; - public CandidateService(ISearchForNewAlbum albumSearchService, + public CandidateService(ISearchForNewBook albumSearchService, IArtistService artistService, IAlbumService albumService, - IReleaseService releaseService, IMediaFileService mediaFileService, Logger logger) { _albumSearchService = albumSearchService; _artistService = artistService; _albumService = albumService; - _releaseService = releaseService; _mediaFileService = mediaFileService; _logger = logger; } @@ -49,45 +44,38 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification // We've tried to make sure that tracks are all for a single release. List candidateReleases; - // if we have a release ID, use that - AlbumRelease tagMbidRelease = null; + // if we have a Book ID, use that + Book tagMbidRelease = null; List tagCandidate = null; - var releaseIds = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.ReleaseMBId).Distinct().ToList(); - if (releaseIds.Count == 1 && releaseIds[0].IsNotNullOrWhiteSpace()) - { - _logger.Debug("Selecting release from consensus ForeignReleaseId [{0}]", releaseIds[0]); - tagMbidRelease = _releaseService.GetReleaseByForeignReleaseId(releaseIds[0], true); - - if (tagMbidRelease != null) - { - tagCandidate = GetDbCandidatesByRelease(new List { tagMbidRelease }, includeExisting); - } - } - - if (idOverrides?.AlbumRelease != null) - { - // this case overrides the release picked up from the file tags - var release = idOverrides.AlbumRelease; - _logger.Debug("Release {0} [{1} tracks] was forced", release, release.TrackCount); - candidateReleases = GetDbCandidatesByRelease(new List { release }, includeExisting); - } - else if (idOverrides?.Album != null) + // TODO: select by ISBN? + // var releaseIds = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.ReleaseMBId).Distinct().ToList(); + // if (releaseIds.Count == 1 && releaseIds[0].IsNotNullOrWhiteSpace()) + // { + // _logger.Debug("Selecting release from consensus ForeignReleaseId [{0}]", releaseIds[0]); + // tagMbidRelease = _releaseService.GetReleaseByForeignReleaseId(releaseIds[0], true); + + // if (tagMbidRelease != null) + // { + // tagCandidate = GetDbCandidatesByRelease(new List { tagMbidRelease }, includeExisting); + // } + // } + if (idOverrides?.Album != null) { // use the release from file tags if it exists and agrees with the specified album - if (tagMbidRelease?.AlbumId == idOverrides.Album.Id) + if (tagMbidRelease?.Id == idOverrides.Album.Id) { candidateReleases = tagCandidate; } else { - candidateReleases = GetDbCandidatesByAlbum(localAlbumRelease, idOverrides.Album, includeExisting); + candidateReleases = GetDbCandidatesByAlbum(idOverrides.Album, includeExisting); } } else if (idOverrides?.Artist != null) { // use the release from file tags if it exists and agrees with the specified album - if (tagMbidRelease?.Album.Value.ArtistMetadataId == idOverrides.Artist.ArtistMetadataId) + if (tagMbidRelease?.AuthorMetadataId == idOverrides.Artist.AuthorMetadataId) { candidateReleases = tagCandidate; } @@ -109,36 +97,24 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification } watch.Stop(); - _logger.Debug($"Getting candidates from tags for {localAlbumRelease.LocalTracks.Count} tracks took {watch.ElapsedMilliseconds}ms"); + _logger.Debug($"Getting {candidateReleases.Count} candidates from tags for {localAlbumRelease.LocalTracks.Count} tracks took {watch.ElapsedMilliseconds}ms"); - // if we haven't got any candidates then try fingerprinting return candidateReleases; } - private List GetDbCandidatesByRelease(List releases, bool includeExisting) + private List GetDbCandidatesByAlbum(Book album, bool includeExisting) { - // get the local tracks on disk for each album - var albumTracks = releases.Select(x => x.AlbumId) - .Distinct() - .ToDictionary(id => id, id => includeExisting ? _mediaFileService.GetFilesByAlbum(id) : new List()); - - return releases.Select(x => new CandidateAlbumRelease + return new List { - AlbumRelease = x, - ExistingTracks = albumTracks[x.AlbumId] - }).ToList(); - } - - private List GetDbCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album, bool includeExisting) - { - // sort candidate releases by closest track count so that we stand a chance of - // getting a perfect match early on - return GetDbCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id) - .OrderBy(x => Math.Abs(localAlbumRelease.TrackCount - x.TrackCount)) - .ToList(), includeExisting); + new CandidateAlbumRelease + { + Book = album, + ExistingTracks = includeExisting ? _mediaFileService.GetFilesByAlbum(album.Id) : new List() + } + }; } - private List GetDbCandidatesByArtist(LocalAlbumRelease localAlbumRelease, Artist artist, bool includeExisting) + private List GetDbCandidatesByArtist(LocalAlbumRelease localAlbumRelease, Author artist, bool includeExisting) { _logger.Trace("Getting candidates for {0}", artist); var candidateReleases = new List(); @@ -146,10 +122,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification var albumTag = localAlbumRelease.LocalTracks.MostCommon(x => x.FileTrackInfo.AlbumTitle) ?? ""; if (albumTag.IsNotNullOrWhiteSpace()) { - var possibleAlbums = _albumService.GetCandidates(artist.ArtistMetadataId, albumTag); + var possibleAlbums = _albumService.GetCandidates(artist.AuthorMetadataId, albumTag); foreach (var album in possibleAlbums) { - candidateReleases.AddRange(GetDbCandidatesByAlbum(localAlbumRelease, album, includeExisting)); + candidateReleases.AddRange(GetDbCandidatesByAlbum(album, includeExisting)); } } @@ -165,7 +141,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification // check if it looks like VA. if (TrackGroupingService.IsVariousArtists(localAlbumRelease.LocalTracks)) { - var va = _artistService.FindById(DistanceCalculator.VariousArtistIds[0]); + var va = _artistService.FindById(DistanceCalculator.VariousAuthorIds[0]); if (va != null) { candidateReleases.AddRange(GetDbCandidatesByArtist(localAlbumRelease, va, includeExisting)); @@ -185,65 +161,53 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification return candidateReleases; } - public List GetDbCandidatesFromFingerprint(LocalAlbumRelease localAlbumRelease, IdentificationOverrides idOverrides, bool includeExisting) - { - var recordingIds = localAlbumRelease.LocalTracks.Where(x => x.AcoustIdResults != null).SelectMany(x => x.AcoustIdResults).ToList(); - var allReleases = _releaseService.GetReleasesByRecordingIds(recordingIds); - - // make sure releases are consistent with those selected by the user - if (idOverrides?.AlbumRelease != null) - { - allReleases = allReleases.Where(x => x.Id == idOverrides.AlbumRelease.Id).ToList(); - } - else if (idOverrides?.Album != null) - { - allReleases = allReleases.Where(x => x.AlbumId == idOverrides.Album.Id).ToList(); - } - else if (idOverrides?.Artist != null) - { - allReleases = allReleases.Where(x => x.Album.Value.ArtistMetadataId == idOverrides.Artist.ArtistMetadataId).ToList(); - } - - return GetDbCandidatesByRelease(allReleases.Select(x => new - { - Release = x, - TrackCount = x.TrackCount, - CommonProportion = x.Tracks.Value.Select(y => y.ForeignRecordingId).Intersect(recordingIds).Count() / localAlbumRelease.TrackCount - }) - .Where(x => x.CommonProportion > 0.6) - .ToList() - .OrderBy(x => Math.Abs(x.TrackCount - localAlbumRelease.TrackCount)) - .ThenByDescending(x => x.CommonProportion) - .Select(x => x.Release) - .Take(10) - .ToList(), includeExisting); - } - public List GetRemoteCandidates(LocalAlbumRelease localAlbumRelease) { // Gets candidate album releases from the metadata server. // Will eventually need adding locally if we find a match var watch = System.Diagnostics.Stopwatch.StartNew(); - List remoteAlbums; + List remoteAlbums = null; var candidates = new List(); - var albumIds = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.AlbumMBId).Distinct().ToList(); - var recordingIds = localAlbumRelease.LocalTracks.Where(x => x.AcoustIdResults != null).SelectMany(x => x.AcoustIdResults).Distinct().ToList(); + var goodreads = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.GoodreadsId).Distinct().ToList(); + var isbns = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.Isbn).Distinct().ToList(); + var asins = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.Asin).Distinct().ToList(); try { - if (albumIds.Count == 1 && albumIds[0].IsNotNullOrWhiteSpace()) + if (goodreads.Count == 1 && goodreads[0].IsNotNullOrWhiteSpace()) { - // Use mbids in tags if set - remoteAlbums = _albumSearchService.SearchForNewAlbum($"mbid:{albumIds[0]}", null); + if (int.TryParse(goodreads[0], out var id)) + { + _logger.Trace($"Searching by goodreads id {id}"); + + remoteAlbums = _albumSearchService.SearchByGoodreadsId(id); + } } - else if (recordingIds.Any()) + + if ((remoteAlbums == null || !remoteAlbums.Any()) && + isbns.Count == 1 && + isbns[0].IsNotNullOrWhiteSpace()) { - // If fingerprints present use those - remoteAlbums = _albumSearchService.SearchForNewAlbumByRecordingIds(recordingIds); + _logger.Trace($"Searching by isbn {isbns[0]}"); + + remoteAlbums = _albumSearchService.SearchByIsbn(isbns[0]); } - else + + // Calibre puts junk asins into books it creates so check for sensible length + if ((remoteAlbums == null || !remoteAlbums.Any()) && + asins.Count == 1 && + asins[0].IsNotNullOrWhiteSpace() && + asins[0].Length == 10) + { + _logger.Trace($"Searching by asin {asins[0]}"); + + remoteAlbums = _albumSearchService.SearchByAsin(asins[0]); + } + + // if no asin/isbn or no result, fall back to text search + if (remoteAlbums == null || !remoteAlbums.Any()) { // fall back to artist / album name search string artistTag; @@ -264,28 +228,30 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification return candidates; } - remoteAlbums = _albumSearchService.SearchForNewAlbum(albumTag, artistTag); + remoteAlbums = _albumSearchService.SearchForNewBook(albumTag, artistTag); + + if (!remoteAlbums.Any()) + { + var albumSearch = _albumSearchService.SearchForNewBook(albumTag, null); + var artistSearch = _albumSearchService.SearchForNewBook(artistTag, null); + + remoteAlbums = albumSearch.Concat(artistSearch).DistinctBy(x => x.ForeignBookId).ToList(); + } } } catch (SkyHookException e) { _logger.Info(e, "Skipping album due to SkyHook error"); - remoteAlbums = new List(); + remoteAlbums = new List(); } foreach (var album in remoteAlbums) { - // We have to make sure various bits and pieces are populated that are normally handled - // by a database lazy load - foreach (var release in album.AlbumReleases.Value) + candidates.Add(new CandidateAlbumRelease { - release.Album = album; - candidates.Add(new CandidateAlbumRelease - { - AlbumRelease = release, - ExistingTracks = new List() - }); - } + Book = album, + ExistingTracks = new List() + }); } watch.Stop(); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/Distance.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/Distance.cs index b0888fa33..f22f62567 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/Distance.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/Distance.cs @@ -122,7 +122,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification return new string(arr); } - public void AddString(string key, string value, string target) + private double StringScore(string value, string target) { // Adds a penaltly based on the distance between value and target var cleanValue = Clean(value); @@ -130,18 +130,33 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification if (cleanValue.IsNullOrWhiteSpace() && cleanTarget.IsNotNullOrWhiteSpace()) { - Add(key, 1.0); + return 1.0; } else if (cleanValue.IsNullOrWhiteSpace() && cleanTarget.IsNullOrWhiteSpace()) { - Add(key, 0.0); + return 0.0; } else { - Add(key, 1.0 - cleanValue.LevenshteinCoefficient(cleanTarget)); + return 1.0 - cleanValue.LevenshteinCoefficient(cleanTarget); } } + public void AddString(string key, string value, string target) + { + Add(key, StringScore(value, target)); + } + + public void AddString(string key, string value, List options) + { + Add(key, options.Min(x => StringScore(value, x))); + } + + public void AddString(string key, List values, string target) + { + Add(key, values.Min(v => StringScore(v, target))); + } + public void AddBool(string key, bool expr) { Add(key, expr ? 1.0 : 0.0); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalcualtor.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalcualtor.cs deleted file mode 100644 index 833ccb12f..000000000 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalcualtor.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Instrumentation; -using NzbDrone.Core.Music; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.TrackImport.Identification -{ - public static class DistanceCalculator - { - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DistanceCalculator)); - - public static readonly List VariousArtistIds = new List { "89ad4ac3-39f7-470e-963a-56509c546377" }; - private static readonly List VariousArtistNames = new List { "various artists", "various", "va", "unknown" }; - private static readonly List PreferredCountries = new List - { - "United States", - "United Kingdom", - "Europe", - "[Worldwide]" - }.Select(x => IsoCountries.Find(x)).ToList(); - - private static bool TrackIndexIncorrect(LocalTrack localTrack, Track mbTrack, int totalTrackNumber) - { - return localTrack.FileTrackInfo.TrackNumbers[0] != mbTrack.AbsoluteTrackNumber && - localTrack.FileTrackInfo.TrackNumbers[0] != totalTrackNumber; - } - - public static int GetTotalTrackNumber(Track track, List allTracks) - { - return track.AbsoluteTrackNumber + allTracks.Count(t => t.MediumNumber < track.MediumNumber); - } - - public static Distance TrackDistance(LocalTrack localTrack, Track mbTrack, int totalTrackNumber, bool includeArtist = false) - { - var dist = new Distance(); - - var localLength = localTrack.FileTrackInfo.Duration.TotalSeconds; - var mbLength = mbTrack.Duration / 1000; - var diff = Math.Abs(localLength - mbLength) - 10; - - if (mbLength > 0) - { - dist.AddRatio("track_length", diff, 30); - } - - // musicbrainz never has 'featuring' in the track title - // see https://musicbrainz.org/doc/Style/Artist_Credits - dist.AddString("track_title", localTrack.FileTrackInfo.CleanTitle ?? "", mbTrack.Title); - - if (includeArtist && localTrack.FileTrackInfo.ArtistTitle.IsNotNullOrWhiteSpace() - && !VariousArtistNames.Any(x => x.Equals(localTrack.FileTrackInfo.ArtistTitle, StringComparison.InvariantCultureIgnoreCase))) - { - dist.AddString("track_artist", localTrack.FileTrackInfo.ArtistTitle, mbTrack.ArtistMetadata.Value.Name); - } - - if (localTrack.FileTrackInfo.TrackNumbers.FirstOrDefault() > 0 && mbTrack.AbsoluteTrackNumber > 0) - { - dist.AddBool("track_index", TrackIndexIncorrect(localTrack, mbTrack, totalTrackNumber)); - } - - var recordingId = localTrack.FileTrackInfo.RecordingMBId; - if (recordingId.IsNotNullOrWhiteSpace()) - { - dist.AddBool("recording_id", localTrack.FileTrackInfo.RecordingMBId != mbTrack.ForeignRecordingId && - !mbTrack.OldForeignRecordingIds.Contains(localTrack.FileTrackInfo.RecordingMBId)); - } - - // for fingerprinted files - if (localTrack.AcoustIdResults != null) - { - dist.AddBool("recording_id", !localTrack.AcoustIdResults.Contains(mbTrack.ForeignRecordingId)); - } - - return dist; - } - - public static Distance AlbumReleaseDistance(List localTracks, AlbumRelease release, TrackMapping mapping) - { - var dist = new Distance(); - - if (!VariousArtistIds.Contains(release.Album.Value.ArtistMetadata.Value.ForeignArtistId)) - { - var artist = localTracks.MostCommon(x => x.FileTrackInfo.ArtistTitle) ?? ""; - dist.AddString("artist", artist, release.Album.Value.ArtistMetadata.Value.Name); - Logger.Trace("artist: {0} vs {1}; {2}", artist, release.Album.Value.ArtistMetadata.Value.Name, dist.NormalizedDistance()); - } - - var title = localTracks.MostCommon(x => x.FileTrackInfo.AlbumTitle) ?? ""; - - // Use the album title since the differences in release titles can cause confusion and - // aren't always correct in the tags - dist.AddString("album", title, release.Album.Value.Title); - Logger.Trace("album: {0} vs {1}; {2}", title, release.Title, dist.NormalizedDistance()); - - // Number of discs, either as tagged or the max disc number seen - var discCount = localTracks.MostCommon(x => x.FileTrackInfo.DiscCount); - discCount = discCount != 0 ? discCount : localTracks.Max(x => x.FileTrackInfo.DiscNumber); - if (discCount > 0) - { - dist.AddNumber("media_count", discCount, release.Media.Count); - Logger.Trace("media_count: {0} vs {1}; {2}", discCount, release.Media.Count, dist.NormalizedDistance()); - } - - // Media format - if (release.Media.Select(x => x.Format).Contains("Unknown")) - { - dist.Add("media_format", 1.0); - } - - // Year - var localYear = localTracks.MostCommon(x => x.FileTrackInfo.Year); - if (localYear > 0 && (release.Album.Value.ReleaseDate.HasValue || release.ReleaseDate.HasValue)) - { - var albumYear = release.Album.Value.ReleaseDate?.Year ?? 0; - var releaseYear = release.ReleaseDate?.Year ?? 0; - if (localYear == albumYear || localYear == releaseYear) - { - dist.Add("year", 0.0); - } - else - { - var remoteYear = albumYear > 0 ? albumYear : releaseYear; - var diff = Math.Abs(localYear - remoteYear); - var diff_max = Math.Abs(DateTime.Now.Year - remoteYear); - dist.AddRatio("year", diff, diff_max); - } - - Logger.Trace($"year: {localYear} vs {release.Album.Value.ReleaseDate?.Year} or {release.ReleaseDate?.Year}; {dist.NormalizedDistance()}"); - } - - // If we parsed a country from the files use that, otherwise use our preference - var country = localTracks.MostCommon(x => x.FileTrackInfo.Country); - if (release.Country.Count > 0) - { - if (country != null) - { - dist.AddEquality("country", country.Name, release.Country); - Logger.Trace("country: {0} vs {1}; {2}", country.Name, string.Join(", ", release.Country), dist.NormalizedDistance()); - } - else if (PreferredCountries.Count > 0) - { - dist.AddPriority("country", release.Country, PreferredCountries.Select(x => x.Name).ToList()); - Logger.Trace("country priority: {0} vs {1}; {2}", string.Join(", ", PreferredCountries.Select(x => x.Name)), string.Join(", ", release.Country), dist.NormalizedDistance()); - } - } - else - { - // full penalty if MusicBrainz release is missing a country - dist.Add("country", 1.0); - } - - var label = localTracks.MostCommon(x => x.FileTrackInfo.Label); - if (label.IsNotNullOrWhiteSpace()) - { - dist.AddEquality("label", label, release.Label); - Logger.Trace("label: {0} vs {1}; {2}", label, string.Join(", ", release.Label), dist.NormalizedDistance()); - } - - var disambig = localTracks.MostCommon(x => x.FileTrackInfo.Disambiguation); - if (disambig.IsNotNullOrWhiteSpace()) - { - dist.AddString("album_disambiguation", disambig, release.Disambiguation); - Logger.Trace("album_disambiguation: {0} vs {1}; {2}", disambig, release.Disambiguation, dist.NormalizedDistance()); - } - - var mbAlbumId = localTracks.MostCommon(x => x.FileTrackInfo.ReleaseMBId); - if (mbAlbumId.IsNotNullOrWhiteSpace()) - { - dist.AddBool("album_id", mbAlbumId != release.ForeignReleaseId && !release.OldForeignReleaseIds.Contains(mbAlbumId)); - Logger.Trace("album_id: {0} vs {1} or {2}; {3}", mbAlbumId, release.ForeignReleaseId, string.Join(", ", release.OldForeignReleaseIds), dist.NormalizedDistance()); - } - - // tracks - foreach (var pair in mapping.Mapping) - { - dist.Add("tracks", pair.Value.Item2.NormalizedDistance()); - } - - Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance()); - - // missing tracks - foreach (var track in mapping.MBExtra.Take(localTracks.Count)) - { - dist.Add("missing_tracks", 1.0); - } - - Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance()); - - // unmatched tracks - foreach (var track in mapping.LocalExtra.Take(localTracks.Count)) - { - dist.Add("unmatched_tracks", 1.0); - } - - Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance()); - - return dist; - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs new file mode 100644 index 000000000..25171f589 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.Music; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.TrackImport.Identification +{ + public static class DistanceCalculator + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DistanceCalculator)); + + public static readonly List VariousAuthorIds = new List { "89ad4ac3-39f7-470e-963a-56509c546377" }; + private static readonly List VariousArtistNames = new List { "various artists", "various", "va", "unknown" }; + private static readonly List PreferredCountries = new List + { + "United States", + "United Kingdom", + "Europe", + "[Worldwide]" + }.Select(x => IsoCountries.Find(x)).ToList(); + + private static readonly RegexReplace StripSeriesRegex = new RegexReplace(@"\([^\)].+?\)$", string.Empty, RegexOptions.Compiled); + + public static Distance BookDistance(List localTracks, Book release) + { + var dist = new Distance(); + + var artists = new List { localTracks.MostCommon(x => x.FileTrackInfo.ArtistTitle) ?? "" }; + + // Add version based on un-reversed + if (artists[0].Contains(',')) + { + artists.Add(artists[0].Split(',').Select(x => x.Trim()).Reverse().ConcatToString(" ")); + } + + dist.AddString("artist", artists, release.AuthorMetadata.Value.Name); + Logger.Trace("artist: '{0}' vs '{1}'; {2}", artists.ConcatToString("' or '"), release.AuthorMetadata.Value.Name, dist.NormalizedDistance()); + + var title = localTracks.MostCommon(x => x.FileTrackInfo.AlbumTitle) ?? ""; + var titleOptions = new List { release.Title }; + if (titleOptions[0].Contains("#")) + { + titleOptions.Add(StripSeriesRegex.Replace(titleOptions[0])); + } + + if (release.SeriesLinks?.Value?.Any() ?? false) + { + foreach (var l in release.SeriesLinks.Value) + { + titleOptions.Add($"{l.Series.Value.Title} {l.Position} {release.Title}"); + titleOptions.Add($"{release.Title} {l.Series.Value.Title} {l.Position}"); + } + } + + dist.AddString("album", title, titleOptions); + Logger.Trace("album: '{0}' vs '{1}'; {2}", title, titleOptions.ConcatToString("' or '"), dist.NormalizedDistance()); + + // Year + var localYear = localTracks.MostCommon(x => x.FileTrackInfo.Year); + if (localYear > 0 && release.ReleaseDate.HasValue) + { + var albumYear = release.ReleaseDate?.Year ?? 0; + if (localYear == albumYear) + { + dist.Add("year", 0.0); + } + else + { + var remoteYear = albumYear; + var diff = Math.Abs(localYear - remoteYear); + var diff_max = Math.Abs(DateTime.Now.Year - remoteYear); + dist.AddRatio("year", diff, diff_max); + } + + Logger.Trace($"year: {localYear} vs {release.ReleaseDate?.Year}; {dist.NormalizedDistance()}"); + } + + return dist; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs index ab98b5cf8..cb4d31daf 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs @@ -1,16 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; using NLog; -using NzbDrone.Common; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.TrackImport.Aggregation; -using NzbDrone.Core.Music; -using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.TrackImport.Identification @@ -22,72 +16,29 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification public class IdentificationService : IIdentificationService { - private readonly ITrackService _trackService; private readonly ITrackGroupingService _trackGroupingService; - private readonly IFingerprintingService _fingerprintingService; private readonly IAudioTagService _audioTagService; private readonly IAugmentingService _augmentingService; private readonly ICandidateService _candidateService; - private readonly IConfigService _configService; private readonly Logger _logger; - public IdentificationService(ITrackService trackService, - ITrackGroupingService trackGroupingService, - IFingerprintingService fingerprintingService, + public IdentificationService(ITrackGroupingService trackGroupingService, IAudioTagService audioTagService, IAugmentingService augmentingService, ICandidateService candidateService, - IConfigService configService, Logger logger) { - _trackService = trackService; _trackGroupingService = trackGroupingService; - _fingerprintingService = fingerprintingService; _audioTagService = audioTagService; _augmentingService = augmentingService; _candidateService = candidateService; - _configService = configService; _logger = logger; } - private void LogTestCaseOutput(List localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease) - { - var trackData = localTracks.Select(x => new BasicLocalTrack - { - Path = x.Path, - FileTrackInfo = x.FileTrackInfo - }); - var options = new IdTestCase - { - ExpectedMusicBrainzReleaseIds = new List { "expected-id-1", "expected-id-2", "..." }, - LibraryArtists = new List - { - new ArtistTestCase - { - Artist = artist?.Metadata.Value.ForeignArtistId ?? "expected-artist-id (dev: don't forget to add metadata profile)", - MetadataProfile = artist?.MetadataProfile.Value - } - }, - Artist = artist?.Metadata.Value.ForeignArtistId, - Album = album?.ForeignAlbumId, - Release = release?.ForeignReleaseId, - NewDownload = newDownload, - SingleRelease = singleRelease, - Tracks = trackData.ToList() - }; - - var serializerSettings = Json.GetSerializerSettings(); - serializerSettings.Formatting = Formatting.None; - - var output = JsonConvert.SerializeObject(options, serializerSettings); - - _logger.Debug($"*** IdentificationService TestCaseGenerator ***\n{output}"); - } - public List GetLocalAlbumReleases(List localTracks, bool singleRelease) { var watch = System.Diagnostics.Stopwatch.StartNew(); - List releases = null; + List releases; if (singleRelease) { releases = new List { new LocalAlbumRelease(localTracks) }; @@ -119,7 +70,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification // 1 group localTracks so that we think they represent a single release // 2 get candidates given specified artist, album and release. Candidates can include extra files already on disk. // 3 find best candidate - // 4 If best candidate worse than threshold, try fingerprinting var watch = System.Diagnostics.Stopwatch.StartNew(); _logger.Debug("Starting track identification"); @@ -130,7 +80,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification foreach (var localRelease in releases) { i++; - _logger.ProgressInfo($"Identifying album {i}/{releases.Count}"); + _logger.ProgressInfo($"Identifying book {i}/{releases.Count}"); IdentifyRelease(localRelease, idOverrides, config); } @@ -141,36 +91,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification return releases; } - private bool FingerprintingAllowed(bool newDownload) - { - if (_configService.AllowFingerprinting == AllowFingerprinting.Never || - (_configService.AllowFingerprinting == AllowFingerprinting.NewFiles && !newDownload)) - { - return false; - } - - return true; - } - - private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease) - { - var worstTrackMatchDist = localAlbumRelease.TrackMapping?.Mapping - .OrderByDescending(x => x.Value.Item2.NormalizedDistance()) - .First() - .Value.Item2.NormalizedDistance() ?? 1.0; - - if (localAlbumRelease.Distance.NormalizedDistance() > 0.15 || - localAlbumRelease.TrackMapping.LocalExtra.Any() || - localAlbumRelease.TrackMapping.MBExtra.Any() || - worstTrackMatchDist > 0.40) - { - return true; - } - - return false; - } - - private List ToLocalTrack(IEnumerable trackfiles, LocalAlbumRelease localRelease) + private List ToLocalTrack(IEnumerable trackfiles, LocalAlbumRelease localRelease) { var scanned = trackfiles.Join(localRelease.LocalTracks, t => t.Path, l => l.Path, (track, localTrack) => localTrack); var toScan = trackfiles.ExceptBy(t => t.Path, scanned, s => s.Path, StringComparer.InvariantCulture); @@ -194,7 +115,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification private void IdentifyRelease(LocalAlbumRelease localAlbumRelease, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config) { var watch = System.Diagnostics.Stopwatch.StartNew(); - bool fingerprinted = false; var candidateReleases = _candidateService.GetDbCandidatesFromTags(localAlbumRelease, idOverrides, config.IncludeExisting); @@ -203,30 +123,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification candidateReleases = _candidateService.GetRemoteCandidates(localAlbumRelease); } - if (candidateReleases.Count == 0 && FingerprintingAllowed(config.NewDownload)) - { - _logger.Debug("No candidates found, fingerprinting"); - _fingerprintingService.Lookup(localAlbumRelease.LocalTracks, 0.5); - fingerprinted = true; - candidateReleases = _candidateService.GetDbCandidatesFromFingerprint(localAlbumRelease, idOverrides, config.IncludeExisting); - - if (candidateReleases.Count == 0 && config.AddNewArtists) - { - // Now fingerprints are populated this will return a different answer - candidateReleases = _candidateService.GetRemoteCandidates(localAlbumRelease); - } - } - if (candidateReleases.Count == 0) { - // can't find any candidates even after fingerprinting return; } _logger.Debug($"Got {candidateReleases.Count} candidates for {localAlbumRelease.LocalTracks.Count} tracks in {watch.ElapsedMilliseconds}ms"); - PopulateTracks(candidateReleases); - // convert all the TrackFiles that represent extra files to List var allLocalTracks = ToLocalTrack(candidateReleases .SelectMany(x => x.ExistingTracks) @@ -236,38 +139,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification GetBestRelease(localAlbumRelease, candidateReleases, allLocalTracks); - // If result isn't great and we haven't fingerprinted, try that - // Note that this can improve the match even if we try the same candidates - if (!fingerprinted && FingerprintingAllowed(config.NewDownload) && ShouldFingerprint(localAlbumRelease)) - { - _logger.Debug($"Match not good enough, fingerprinting"); - _fingerprintingService.Lookup(localAlbumRelease.LocalTracks, 0.5); - - // Only include extra possible candidates if neither album nor release are specified - // Will generally be specified as part of manual import - if (idOverrides?.Album == null && idOverrides?.AlbumRelease == null) - { - var dbCandidates = _candidateService.GetDbCandidatesFromFingerprint(localAlbumRelease, idOverrides, config.IncludeExisting); - var remoteCandidates = config.AddNewArtists ? _candidateService.GetRemoteCandidates(localAlbumRelease) : new List(); - var extraCandidates = dbCandidates.Concat(remoteCandidates); - var newCandidates = extraCandidates.ExceptBy(x => x.AlbumRelease.Id, candidateReleases, y => y.AlbumRelease.Id, EqualityComparer.Default); - candidateReleases.AddRange(newCandidates); - - PopulateTracks(candidateReleases); - - allLocalTracks.AddRange(ToLocalTrack(newCandidates - .SelectMany(x => x.ExistingTracks) - .DistinctBy(x => x.Path) - .ExceptBy(x => x.Path, allLocalTracks, x => x.Path, PathEqualityComparer.Instance), - localAlbumRelease)); - } - - // fingerprint all the local files in candidates we might be matching against - _fingerprintingService.Lookup(allLocalTracks, 0.5); - - GetBestRelease(localAlbumRelease, candidateReleases, allLocalTracks); - } - _logger.Debug($"Best release found in {watch.ElapsedMilliseconds}ms"); localAlbumRelease.PopulateMatch(); @@ -275,21 +146,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification _logger.Debug($"IdentifyRelease done in {watch.ElapsedMilliseconds}ms"); } - public void PopulateTracks(List candidateReleases) - { - var watch = System.Diagnostics.Stopwatch.StartNew(); - - var releasesMissingTracks = candidateReleases.Where(x => !x.AlbumRelease.Tracks.IsLoaded); - var allTracks = _trackService.GetTracksByReleases(releasesMissingTracks.Select(x => x.AlbumRelease.Id).ToList()); - - _logger.Debug($"Retrieved {allTracks.Count} possible tracks in {watch.ElapsedMilliseconds}ms"); - - foreach (var release in releasesMissingTracks) - { - release.AlbumRelease.Tracks = allTracks.Where(x => x.AlbumReleaseId == release.AlbumRelease.Id).ToList(); - } - } - private void GetBestRelease(LocalAlbumRelease localAlbumRelease, List candidateReleases, List extraTracksOnDisk) { var watch = System.Diagnostics.Stopwatch.StartNew(); @@ -301,22 +157,20 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification foreach (var candidateRelease in candidateReleases) { - var release = candidateRelease.AlbumRelease; - _logger.Debug("Trying Release {0} [{1}, {2} tracks, {3} existing]", release, release.Title, release.TrackCount, candidateRelease.ExistingTracks.Count); + var release = candidateRelease.Book; + _logger.Debug($"Trying Release {release}"); var rwatch = System.Diagnostics.Stopwatch.StartNew(); var extraTrackPaths = candidateRelease.ExistingTracks.Select(x => x.Path).ToList(); var extraTracks = extraTracksOnDisk.Where(x => extraTrackPaths.Contains(x.Path)).ToList(); var allLocalTracks = localAlbumRelease.LocalTracks.Concat(extraTracks).DistinctBy(x => x.Path).ToList(); - var mapping = MapReleaseTracks(allLocalTracks, release.Tracks.Value); - var distance = DistanceCalculator.AlbumReleaseDistance(allLocalTracks, release, mapping); + var distance = DistanceCalculator.BookDistance(allLocalTracks, release); var currDistance = distance.NormalizedDistance(); rwatch.Stop(); - _logger.Debug("Release {0} [{1} tracks] has distance {2} vs best distance {3} [{4}ms]", + _logger.Debug("Release {0} has distance {1} vs best distance {2} [{3}ms]", release, - release.TrackCount, currDistance, bestDistance, rwatch.ElapsedMilliseconds); @@ -324,9 +178,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification { bestDistance = currDistance; localAlbumRelease.Distance = distance; - localAlbumRelease.AlbumRelease = release; + localAlbumRelease.Book = release; localAlbumRelease.ExistingTracks = extraTracks; - localAlbumRelease.TrackMapping = mapping; if (currDistance == 0.0) { break; @@ -335,41 +188,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification } watch.Stop(); - _logger.Debug($"Best release: {localAlbumRelease.AlbumRelease} Distance {localAlbumRelease.Distance.NormalizedDistance()} found in {watch.ElapsedMilliseconds}ms"); - } - - public TrackMapping MapReleaseTracks(List localTracks, List mbTracks) - { - var distances = new Distance[localTracks.Count, mbTracks.Count]; - var costs = new double[localTracks.Count, mbTracks.Count]; - - for (int col = 0; col < mbTracks.Count; col++) - { - var totalTrackNumber = DistanceCalculator.GetTotalTrackNumber(mbTracks[col], mbTracks); - for (int row = 0; row < localTracks.Count; row++) - { - distances[row, col] = DistanceCalculator.TrackDistance(localTracks[row], mbTracks[col], totalTrackNumber, false); - costs[row, col] = distances[row, col].NormalizedDistance(); - } - } - - var m = new Munkres(costs); - m.Run(); - - var result = new TrackMapping(); - foreach (var pair in m.Solution) - { - result.Mapping.Add(localTracks[pair.Item1], Tuple.Create(mbTracks[pair.Item2], distances[pair.Item1, pair.Item2])); - _logger.Trace("Mapped {0} to {1}, dist: {2}", localTracks[pair.Item1], mbTracks[pair.Item2], costs[pair.Item1, pair.Item2]); - } - - result.LocalExtra = localTracks.Except(result.Mapping.Keys).ToList(); - _logger.Trace($"Unmapped files:\n{string.Join("\n", result.LocalExtra)}"); - - result.MBExtra = mbTracks.Except(result.Mapping.Values.Select(x => x.Item1)).ToList(); - _logger.Trace($"Missing tracks:\n{string.Join("\n", result.MBExtra)}"); - - return result; + _logger.Debug($"Best release: {localAlbumRelease.Book} Distance {localAlbumRelease.Distance.NormalizedDistance()} found in {watch.ElapsedMilliseconds}ms"); } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/TrackGroupingService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/TrackGroupingService.cs index 85b17098a..7607951b6 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/TrackGroupingService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/TrackGroupingService.cs @@ -31,9 +31,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification var releases = new List(); + // text files are always single file releases + var textfiles = localTracks.Where(x => MediaFileExtensions.TextExtensions.Contains(Path.GetExtension(x.Path))); + foreach (var file in textfiles) + { + releases.Add(new LocalAlbumRelease(new List { file })); + } + // first attempt, assume grouped by folder var unprocessed = new List(); - foreach (var group in GroupTracksByDirectory(localTracks)) + foreach (var group in GroupTracksByDirectory(localTracks.Except(textfiles).ToList())) { var tracks = group.ToList(); if (LooksLikeSingleRelease(tracks)) diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs index 781f40a89..52d17aa1b 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; using NzbDrone.Core.Music.Commands; using NzbDrone.Core.Music.Events; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.RootFolders; @@ -30,16 +31,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport private readonly IUpgradeMediaFiles _trackFileUpgrader; private readonly IMediaFileService _mediaFileService; private readonly IAudioTagService _audioTagService; - private readonly ITrackService _trackService; private readonly IArtistService _artistService; private readonly IAddArtistService _addArtistService; private readonly IAlbumService _albumService; - private readonly IRefreshAlbumService _refreshAlbumService; private readonly IRootFolderService _rootFolderService; private readonly IRecycleBinProvider _recycleBinProvider; private readonly IExtraService _extraService; private readonly IDiskProvider _diskProvider; - private readonly IReleaseService _releaseService; private readonly IEventAggregator _eventAggregator; private readonly IManageCommandQueue _commandQueueManager; private readonly Logger _logger; @@ -47,16 +45,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public ImportApprovedTracks(IUpgradeMediaFiles trackFileUpgrader, IMediaFileService mediaFileService, IAudioTagService audioTagService, - ITrackService trackService, IArtistService artistService, IAddArtistService addArtistService, IAlbumService albumService, - IRefreshAlbumService refreshAlbumService, IRootFolderService rootFolderService, IRecycleBinProvider recycleBinProvider, IExtraService extraService, IDiskProvider diskProvider, - IReleaseService releaseService, IEventAggregator eventAggregator, IManageCommandQueue commandQueueManager, Logger logger) @@ -64,16 +59,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport _trackFileUpgrader = trackFileUpgrader; _mediaFileService = mediaFileService; _audioTagService = audioTagService; - _trackService = trackService; _artistService = artistService; _addArtistService = addArtistService; _albumService = albumService; - _refreshAlbumService = refreshAlbumService; _rootFolderService = rootFolderService; _recycleBinProvider = recycleBinProvider; _extraService = extraService; _diskProvider = diskProvider; - _releaseService = releaseService; _eventAggregator = eventAggregator; _commandQueueManager = commandQueueManager; _logger = logger; @@ -82,17 +74,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public List Import(List> decisions, bool replaceExisting, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto) { var importResults = new List(); - var allImportedTrackFiles = new List(); - var allOldTrackFiles = new List(); - var addedArtists = new List(); + var allImportedTrackFiles = new List(); + var allOldTrackFiles = new List(); + var addedArtists = new List(); var albumDecisions = decisions.Where(e => e.Item.Album != null && e.Approved) - .GroupBy(e => e.Item.Album.ForeignAlbumId).ToList(); + .GroupBy(e => e.Item.Album.ForeignBookId).ToList(); int iDecision = 1; foreach (var albumDecision in albumDecisions) { - _logger.ProgressInfo($"Importing album {iDecision++}/{albumDecisions.Count}"); + _logger.ProgressInfo($"Importing book {iDecision++}/{albumDecisions.Count} {albumDecision.First().Item.Album}"); var decisionList = albumDecision.ToList(); @@ -117,11 +109,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport RemoveExistingTrackFiles(artist, album); } - // set the correct release to be monitored before importing the new files - var newRelease = albumDecision.First().Item.Release; - _logger.Debug("Updating release to {0} [{1} tracks]", newRelease, newRelease.TrackCount); - album.AlbumReleases = _releaseService.SetMonitored(newRelease); - // Publish album edited event. // Deliberatly don't put in the old album since we don't want to trigger an ArtistScan. _eventAggregator.PublishEvent(new AlbumEditedEvent(album, album)); @@ -134,53 +121,29 @@ namespace NzbDrone.Core.MediaFiles.TrackImport .SelectMany(c => c) .ToList(); - _logger.ProgressInfo($"Importing {qualifiedImports.Count} tracks"); + _logger.ProgressInfo($"Importing {qualifiedImports.Count} files"); _logger.Debug($"Importing {qualifiedImports.Count} files. replaceExisting: {replaceExisting}"); - var filesToAdd = new List(qualifiedImports.Count); - var albumReleasesDict = new Dictionary>(albumDecisions.Count); + var filesToAdd = new List(qualifiedImports.Count); var trackImportedEvents = new List(qualifiedImports.Count); - foreach (var importDecision in qualifiedImports.OrderBy(e => e.Item.Tracks.Select(track => track.AbsoluteTrackNumber).MinOrDefault()) - .ThenByDescending(e => e.Item.Size)) + foreach (var importDecision in qualifiedImports.OrderByDescending(e => e.Item.Size)) { var localTrack = importDecision.Item; - var oldFiles = new List(); + var oldFiles = new List(); try { //check if already imported - if (importResults.SelectMany(r => r.ImportDecision.Item.Tracks) - .Select(e => e.Id) - .Intersect(localTrack.Tracks.Select(e => e.Id)) - .Any()) + if (importResults.Select(r => r.ImportDecision.Item.Album.Id).Contains(localTrack.Album.Id)) { - importResults.Add(new ImportResult(importDecision, "Track has already been imported")); + importResults.Add(new ImportResult(importDecision, "Book has already been imported")); continue; } - // cache album releases and set artist to speed up firing the TrackImported events - // (otherwise they'll be retrieved from the DB for each track) - if (!albumReleasesDict.ContainsKey(localTrack.Album.Id)) - { - albumReleasesDict.Add(localTrack.Album.Id, localTrack.Album.AlbumReleases.Value); - } - - if (!localTrack.Album.AlbumReleases.IsLoaded) - { - localTrack.Album.AlbumReleases = albumReleasesDict[localTrack.Album.Id]; - } - - localTrack.Album.Artist = localTrack.Artist; - - foreach (var track in localTrack.Tracks) - { - track.Artist = localTrack.Artist; - track.AlbumRelease = localTrack.Release; - track.Album = localTrack.Album; - } + localTrack.Album.Author = localTrack.Artist; - var trackFile = new TrackFile + var trackFile = new BookFile { Path = localTrack.Path.CleanFilePath(), Size = localTrack.Size, @@ -189,10 +152,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport ReleaseGroup = localTrack.ReleaseGroup, Quality = localTrack.Quality, MediaInfo = localTrack.FileTrackInfo.MediaInfo, - AlbumId = localTrack.Album.Id, + BookId = localTrack.Album.Id, Artist = localTrack.Artist, - Album = localTrack.Album, - Tracks = localTrack.Tracks + Album = localTrack.Album }; bool copyOnly; @@ -227,6 +189,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport _mediaFileService.Delete(previousFile, DeleteMediaFileReason.ManualOverride); } + var rootFolder = _rootFolderService.GetBestRootFolder(localTrack.Path); + if (rootFolder.IsCalibreLibrary) + { + trackFile.CalibreId = trackFile.Path.ParseCalibreId(); + } + _audioTagService.WriteTags(trackFile, false); } @@ -275,9 +243,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport watch.Start(); _mediaFileService.AddMany(filesToAdd); _logger.Debug($"Inserted new trackfiles in {watch.ElapsedMilliseconds}ms"); - filesToAdd.ForEach(f => f.Tracks.Value.ForEach(t => t.TrackFileId = f.Id)); - _trackService.SetFileIds(filesToAdd.SelectMany(x => x.Tracks.Value).ToList()); - _logger.Debug($"TrackFileIds updated, total {watch.ElapsedMilliseconds}ms"); // now that trackfiles have been inserted and ids generated, publish the import events foreach (var trackImportedEvent in trackImportedEvents) @@ -290,7 +255,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport foreach (var albumImport in albumImports) { - var release = albumImport.First().ImportDecision.Item.Release; var album = albumImport.First().ImportDecision.Item.Album; var artist = albumImport.First().ImportDecision.Item.Artist; @@ -299,9 +263,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport _eventAggregator.PublishEvent(new AlbumImportedEvent( artist, album, - release, - allImportedTrackFiles.Where(s => s.AlbumId == album.Id).ToList(), - allOldTrackFiles.Where(s => s.AlbumId == album.Id).ToList(), + allImportedTrackFiles.Where(s => s.BookId == album.Id).ToList(), + allOldTrackFiles.Where(s => s.BookId == album.Id).ToList(), replaceExisting, downloadClientItem)); } @@ -320,23 +283,23 @@ namespace NzbDrone.Core.MediaFiles.TrackImport return importResults; } - private Artist EnsureArtistAdded(List> decisions, List addedArtists) + private Author EnsureArtistAdded(List> decisions, List addedArtists) { var artist = decisions.First().Item.Artist; if (artist.Id == 0) { - var dbArtist = _artistService.FindById(artist.ForeignArtistId); + var dbArtist = _artistService.FindById(artist.ForeignAuthorId); if (dbArtist == null) { _logger.Debug($"Adding remote artist {artist}"); - var rootFolder = _rootFolderService.GetBestRootFolder(decisions.First().Item.Path); + var path = decisions.First().Item.Path; + var rootFolder = _rootFolderService.GetBestRootFolder(path); artist.RootFolderPath = rootFolder.Path; artist.MetadataProfileId = rootFolder.DefaultMetadataProfileId; artist.QualityProfileId = rootFolder.DefaultQualityProfileId; - artist.AlbumFolder = true; artist.Monitored = rootFolder.DefaultMonitorOption != MonitorTypes.None; artist.Tags = rootFolder.DefaultTags; artist.AddOptions = new AddArtistOptions @@ -346,6 +309,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport Monitor = rootFolder.DefaultMonitorOption }; + if (rootFolder.IsCalibreLibrary) + { + // calibre has artist / book / files + artist.Path = path.GetParentPath().GetParentPath(); + } + try { dbArtist = _addArtistService.AddArtist(artist, false); @@ -367,8 +336,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport foreach (var decision in decisions) { decision.Item.Artist = dbArtist; - decision.Item.Album.Artist = dbArtist; - decision.Item.Album.ArtistMetadataId = dbArtist.ArtistMetadataId; + decision.Item.Album.Author = dbArtist; + decision.Item.Album.AuthorMetadataId = dbArtist.AuthorMetadataId; } artist = dbArtist; @@ -377,22 +346,22 @@ namespace NzbDrone.Core.MediaFiles.TrackImport return artist; } - private Album EnsureAlbumAdded(List> decisions) + private Book EnsureAlbumAdded(List> decisions) { var album = decisions.First().Item.Album; if (album.Id == 0) { - var dbAlbum = _albumService.FindById(album.ForeignAlbumId); + var dbAlbum = _albumService.FindById(album.ForeignBookId); if (dbAlbum == null) { _logger.Debug($"Adding remote album {album}"); try { - _albumService.InsertMany(new List { album }); - _refreshAlbumService.RefreshAlbumInfo(album, new List { album }, false); - dbAlbum = _albumService.FindById(album.ForeignAlbumId); + album.Added = DateTime.UtcNow; + _albumService.InsertMany(new List { album }); + dbAlbum = _albumService.FindById(album.ForeignBookId); } catch (Exception e) { @@ -403,20 +372,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport } } - var release = dbAlbum.AlbumReleases.Value.ExclusiveOrDefault(x => x.ForeignReleaseId == decisions.First().Item.Release.ForeignReleaseId); - if (release == null) - { - RejectAlbum(decisions); - return null; - } - // Populate the new DB album foreach (var decision in decisions) { decision.Item.Album = dbAlbum; - decision.Item.Release = release; - var trackIds = decision.Item.Tracks.Select(x => x.ForeignTrackId).ToList(); - decision.Item.Tracks = release.Tracks.Value.Where(x => trackIds.Contains(x.ForeignTrackId)).ToList(); } } @@ -431,7 +390,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport } } - private void RemoveExistingTrackFiles(Artist artist, Album album) + private void RemoveExistingTrackFiles(Author artist, Book album) { var rootFolder = _diskProvider.GetParentFolder(artist.Path); var previousFiles = _mediaFileService.GetFilesByAlbum(album.Id); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index 10dd4b27f..794730a70 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -23,9 +23,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public class IdentificationOverrides { - public Artist Artist { get; set; } - public Album Album { get; set; } - public AlbumRelease AlbumRelease { get; set; } + public Author Artist { get; set; } + public Book Album { get; set; } } public class ImportDecisionMakerInfo @@ -48,6 +47,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport private readonly IEnumerable> _trackSpecifications; private readonly IEnumerable> _albumSpecifications; private readonly IMediaFileService _mediaFileService; + private readonly IEBookTagService _eBookTagService; private readonly IAudioTagService _audioTagService; private readonly IAugmentingService _augmentingService; private readonly IIdentificationService _identificationService; @@ -58,6 +58,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public ImportDecisionMaker(IEnumerable> trackSpecifications, IEnumerable> albumSpecifications, IMediaFileService mediaFileService, + IEBookTagService eBookTagService, IAudioTagService audioTagService, IAugmentingService augmentingService, IIdentificationService identificationService, @@ -68,6 +69,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport _trackSpecifications = trackSpecifications; _albumSpecifications = albumSpecifications; _mediaFileService = mediaFileService; + _eBookTagService = eBookTagService; _audioTagService = audioTagService; _augmentingService = augmentingService; _identificationService = identificationService; @@ -112,7 +114,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport Path = file.FullName, Size = file.Length, Modified = file.LastWriteTimeUtc, - FileTrackInfo = _audioTagService.ReadTags(file.FullName), + FileTrackInfo = _eBookTagService.ReadTags(file), AdditionalFile = false }; @@ -179,12 +181,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport private void EnsureData(LocalAlbumRelease release) { - if (release.AlbumRelease != null && release.AlbumRelease.Album.Value.Artist.Value.QualityProfileId == 0) + if (release.Book != null && release.Book.Author.Value.QualityProfileId == 0) { var rootFolder = _rootFolderService.GetBestRootFolder(release.LocalTracks.First().Path); var qualityProfile = _qualityProfileService.Get(rootFolder.DefaultQualityProfileId); - var artist = release.AlbumRelease.Album.Value.Artist.Value; + var artist = release.Book.Author.Value; artist.QualityProfileId = qualityProfile.Id; artist.QualityProfile = qualityProfile; } @@ -194,9 +196,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport { ImportDecision decision = null; - if (localAlbumRelease.AlbumRelease == null) + if (localAlbumRelease.Book == null) { - decision = new ImportDecision(localAlbumRelease, new Rejection($"Couldn't find similar album for {localAlbumRelease}")); + decision = new ImportDecision(localAlbumRelease, new Rejection($"Couldn't find similar book for {localAlbumRelease}")); } else { @@ -212,11 +214,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport } else if (decision.Rejections.Any()) { - _logger.Debug("Album rejected for the following reasons: {0}", string.Join(", ", decision.Rejections)); + _logger.Debug("Book rejected for the following reasons: {0}", string.Join(", ", decision.Rejections)); } else { - _logger.Debug("Album accepted"); + _logger.Debug("Book accepted"); } return decision; @@ -226,10 +228,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport { ImportDecision decision = null; - if (localTrack.Tracks.Empty()) + if (localTrack.Album == null) { - decision = localTrack.Album != null ? new ImportDecision(localTrack, new Rejection($"Couldn't parse track from: {localTrack.FileTrackInfo}")) : - new ImportDecision(localTrack, new Rejection($"Couldn't parse album from: {localTrack.FileTrackInfo}")); + decision = new ImportDecision(localTrack, new Rejection($"Couldn't parse album from: {localTrack.FileTrackInfo}")); } else { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs index 9faec9a65..58d28466f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Core.Qualities; @@ -8,13 +7,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public class ManualImportFile : IEquatable { public string Path { get; set; } - public int ArtistId { get; set; } - public int AlbumId { get; set; } - public int AlbumReleaseId { get; set; } - public List TrackIds { get; set; } + public int AuthorId { get; set; } + public int BookId { get; set; } public QualityModel Quality { get; set; } public string DownloadId { get; set; } - public bool DisableReleaseSwitching { get; set; } public bool Equals(ManualImportFile other) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs index f0878b774..e11e1a62b 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs @@ -11,22 +11,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { public ManualImportItem() { - Tracks = new List(); } public string Path { get; set; } public string Name { get; set; } public long Size { get; set; } - public Artist Artist { get; set; } - public Album Album { get; set; } - public AlbumRelease Release { get; set; } - public List Tracks { get; set; } + public Author Artist { get; set; } + public Book Album { get; set; } public QualityModel Quality { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } public ParsedTrackInfo Tags { get; set; } public bool AdditionalFile { get; set; } public bool ReplaceExistingFiles { get; set; } - public bool DisableReleaseSwitching { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 6c621e57c..1e4733beb 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -36,8 +36,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual private readonly IMakeImportDecision _importDecisionMaker; private readonly IArtistService _artistService; private readonly IAlbumService _albumService; - private readonly IReleaseService _releaseService; - private readonly ITrackService _trackService; private readonly IAudioTagService _audioTagService; private readonly IImportApprovedTracks _importApprovedTracks; private readonly ITrackedDownloadService _trackedDownloadService; @@ -52,8 +50,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual IMakeImportDecision importDecisionMaker, IArtistService artistService, IAlbumService albumService, - IReleaseService releaseService, - ITrackService trackService, IAudioTagService audioTagService, IImportApprovedTracks importApprovedTracks, ITrackedDownloadService trackedDownloadService, @@ -68,8 +64,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual _importDecisionMaker = importDecisionMaker; _artistService = artistService; _albumService = albumService; - _releaseService = releaseService; - _trackService = trackService; _audioTagService = audioTagService; _importApprovedTracks = importApprovedTracks; _trackedDownloadService = trackedDownloadService; @@ -111,7 +105,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual }; var decision = _importDecisionMaker.GetImportDecisions(files, null, null, config); - var result = MapItem(decision.First(), downloadId, replaceExistingFiles, false); + var result = MapItem(decision.First(), downloadId, replaceExistingFiles); return new List { result }; } @@ -164,9 +158,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual (f, d) => new { File = f, Decision = d }, PathEqualityComparer.Instance); - var newItems = newFiles.Select(x => MapItem(x.Decision, downloadId, replaceExistingFiles, false)); + var newItems = newFiles.Select(x => MapItem(x.Decision, downloadId, replaceExistingFiles)); var existingDecisions = decisions.Except(newFiles.Select(x => x.Decision)); - var existingItems = existingDecisions.Select(x => MapItem(x, null, replaceExistingFiles, false)); + var existingItems = existingDecisions.Select(x => MapItem(x, null, replaceExistingFiles)); return newItems.Concat(existingItems).ToList(); } @@ -183,14 +177,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { _logger.Debug("UpdateItems, group key: {0}", group.Key); - var disableReleaseSwitching = group.First().DisableReleaseSwitching; - var files = group.Select(x => _diskProvider.GetFileInfo(x.Path)).ToList(); var idOverride = new IdentificationOverrides { Artist = group.First().Artist, Album = group.First().Album, - AlbumRelease = group.First().Release }; var config = new ImportDecisionMakerConfig { @@ -221,12 +212,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual if (decision.Item.Album != null) { item.Album = decision.Item.Album; - item.Release = decision.Item.Release; - } - - if (decision.Item.Tracks.Any()) - { - item.Tracks = decision.Item.Tracks; } item.Rejections = decision.Rejections; @@ -235,13 +220,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual } var newDecisions = decisions.Except(existingItems.Select(x => x.Decision)); - result.AddRange(newDecisions.Select(x => MapItem(x, null, replaceExistingFiles, disableReleaseSwitching))); + result.AddRange(newDecisions.Select(x => MapItem(x, null, replaceExistingFiles))); } return result; } - private ManualImportItem MapItem(ImportDecision decision, string downloadId, bool replaceExistingFiles, bool disableReleaseSwitching) + private ManualImportItem MapItem(ImportDecision decision, string downloadId, bool replaceExistingFiles) { var item = new ManualImportItem(); @@ -258,12 +243,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual if (decision.Item.Album != null) { item.Album = decision.Item.Album; - item.Release = decision.Item.Release; - } - - if (decision.Item.Tracks.Any()) - { - item.Tracks = decision.Item.Tracks; } item.Quality = decision.Item.Quality; @@ -272,7 +251,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual item.Tags = decision.Item.FileTrackInfo; item.AdditionalFile = decision.Item.AdditionalFile; item.ReplaceExistingFiles = replaceExistingFiles; - item.DisableReleaseSwitching = disableReleaseSwitching; return item; } @@ -283,44 +261,32 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var imported = new List(); var importedTrackedDownload = new List(); - var albumIds = message.Files.GroupBy(e => e.AlbumId).ToList(); + var bookIds = message.Files.GroupBy(e => e.BookId).ToList(); var fileCount = 0; - foreach (var importAlbumId in albumIds) + foreach (var importBookId in bookIds) { var albumImportDecisions = new List>(); - // turn off anyReleaseOk if specified - if (importAlbumId.First().DisableReleaseSwitching) - { - var album = _albumService.GetAlbum(importAlbumId.First().AlbumId); - album.AnyReleaseOk = false; - _albumService.UpdateAlbum(album); - } - - foreach (var file in importAlbumId) + foreach (var file in importBookId) { _logger.ProgressTrace("Processing file {0} of {1}", fileCount + 1, message.Files.Count); - var artist = _artistService.GetArtist(file.ArtistId); - var album = _albumService.GetAlbum(file.AlbumId); - var release = _releaseService.GetRelease(file.AlbumReleaseId); - var tracks = _trackService.GetTracks(file.TrackIds); + var artist = _artistService.GetArtist(file.AuthorId); + var album = _albumService.GetAlbum(file.BookId); var fileTrackInfo = _audioTagService.ReadTags(file.Path) ?? new ParsedTrackInfo(); var fileInfo = _diskProvider.GetFileInfo(file.Path); var localTrack = new LocalTrack { ExistingFile = artist.Path.IsParentPath(file.Path), - Tracks = tracks, FileTrackInfo = fileTrackInfo, Path = file.Path, Size = fileInfo.Length, Modified = fileInfo.LastWriteTimeUtc, Quality = file.Quality, Artist = artist, - Album = album, - Release = release + Album = album }; var importDecision = new ImportDecision(localTrack); @@ -334,7 +300,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual fileCount += 1; } - var downloadId = importAlbumId.Select(x => x.DownloadId).FirstOrDefault(x => x.IsNotNullOrWhiteSpace()); + var downloadId = importBookId.Select(x => x.DownloadId).FirstOrDefault(x => x.IsNotNullOrWhiteSpace()); if (downloadId.IsNullOrWhiteSpace()) { imported.AddRange(_importApprovedTracks.Import(albumImportDecisions, message.ReplaceExistingFiles, null, message.ImportMode)); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlbumUpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlbumUpgradeSpecification.cs index f6deffbc9..d11acc2ad 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlbumUpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlbumUpgradeSpecification.cs @@ -18,33 +18,24 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) { - // check if we are changing release - var currentRelease = item.AlbumRelease.Album.Value.AlbumReleases.Value.Single(x => x.Monitored); - var newRelease = item.AlbumRelease; + var qualityComparer = new QualityModelComparer(item.Book.Author.Value.QualityProfile); - // if we are, check we are upgrading - if (newRelease.Id != currentRelease.Id) - { - var qualityComparer = new QualityModelComparer(item.AlbumRelease.Album.Value.Artist.Value.QualityProfile); - - // min quality of all new tracks - var newMinQuality = item.LocalTracks.Select(x => x.Quality).OrderBy(x => x, qualityComparer).First(); - _logger.Debug("Min quality of new files: {0}", newMinQuality); - - // get minimum quality of existing release - var existingQualities = currentRelease.Tracks.Value.Where(x => x.TrackFileId != 0).Select(x => x.TrackFile.Value.Quality); - if (existingQualities.Any()) - { - var existingMinQuality = existingQualities.OrderBy(x => x, qualityComparer).First(); - _logger.Debug("Min quality of existing files: {0}", existingMinQuality); - if (qualityComparer.Compare(existingMinQuality, newMinQuality) > 0) - { - _logger.Debug("This album isn't a quality upgrade for all tracks. Skipping {0}", item); - return Decision.Reject("Not an upgrade for existing album file(s)"); - } - } - } + // min quality of all new tracks + var newMinQuality = item.LocalTracks.Select(x => x.Quality).OrderBy(x => x, qualityComparer).First(); + _logger.Debug("Min quality of new files: {0}", newMinQuality); + // get minimum quality of existing release + // var existingQualities = currentRelease.Value.Where(x => x.TrackFileId != 0).Select(x => x.TrackFile.Value.Quality); + // if (existingQualities.Any()) + // { + // var existingMinQuality = existingQualities.OrderBy(x => x, qualityComparer).First(); + // _logger.Debug("Min quality of existing files: {0}", existingMinQuality); + // if (qualityComparer.Compare(existingMinQuality, newMinQuality) > 0) + // { + // _logger.Debug("This album isn't a quality upgrade for all tracks. Skipping {0}", item); + // return Decision.Reject("Not an upgrade for existing album file(s)"); + // } + // } return Decision.Accept(); } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlreadyImportedSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlreadyImportedSpecification.cs index 3f564ea7f..c89bdc7b8 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlreadyImportedSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlreadyImportedSpecification.cs @@ -31,15 +31,15 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } - var albumRelease = localAlbumRelease.AlbumRelease; + var albumRelease = localAlbumRelease.Book; - if (!albumRelease.Tracks.Value.Any(x => x.HasFile)) + if ((!albumRelease?.BookFiles?.Value?.Any()) ?? true) { - _logger.Debug("Skipping already imported check for album without files"); + _logger.Debug("Skipping already imported check for book without files"); return Decision.Accept(); } - var albumHistory = _historyService.GetByAlbum(albumRelease.AlbumId, null); + var albumHistory = _historyService.GetByAlbum(albumRelease.Id, null); var lastImported = albumHistory.FirstOrDefault(h => h.EventType == HistoryEventType.DownloadImported); var lastGrabbed = albumHistory.FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed); @@ -55,8 +55,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (lastImported.DownloadId == downloadClientItem.DownloadId) { - _logger.Debug("Album previously imported at {0}", lastImported.Date); - return Decision.Reject("Album already imported at {0}", lastImported.Date); + _logger.Debug("Book previously imported at {0}", lastImported.Date); + return Decision.Reject("Book already imported at {0}", lastImported.Date); } return Decision.Accept(); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/ArtistPathInRootFolderSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/ArtistPathInRootFolderSpecification.cs index 2b8b2a291..74df21c42 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/ArtistPathInRootFolderSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/ArtistPathInRootFolderSpecification.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) { // Prevent imports to artists that are no longer inside a root folder Readarr manages - var artist = item.AlbumRelease.Album.Value.Artist.Value; + var artist = item.Book.Author.Value; // a new artist will have empty path, and will end up having path assinged based on file location var pathToCheck = artist.Path.IsNotNullOrWhiteSpace() ? artist.Path : item.LocalTracks.First().Path.GetParentPath(); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs index 0db4334d5..6eb9a6f0f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; @@ -10,7 +9,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public class CloseAlbumMatchSpecification : IImportDecisionEngineSpecification { private const double _albumThreshold = 0.20; - private const double _trackThreshold = 0.40; private readonly Logger _logger; public CloseAlbumMatchSpecification(Logger logger) @@ -33,23 +31,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications _logger.Debug($"Album match is not close enough: {dist} vs {_albumThreshold} {reasons}. Skipping {item}"); return Decision.Reject($"Album match is not close enough: {1 - dist:P1} vs {1 - _albumThreshold:P0} {reasons}"); } - - var worstTrackMatch = item.LocalTracks.Where(x => x.Distance != null).OrderByDescending(x => x.Distance.NormalizedDistance()).FirstOrDefault(); - if (worstTrackMatch == null) - { - _logger.Debug($"No tracks matched"); - return Decision.Reject("No tracks matched"); - } - else - { - var maxTrackDist = worstTrackMatch.Distance.NormalizedDistance(); - var trackReasons = worstTrackMatch.Distance.Reasons; - if (maxTrackDist > _trackThreshold) - { - _logger.Debug($"Worst track match: {maxTrackDist} vs {_trackThreshold} {trackReasons}. Skipping {item}"); - return Decision.Reject($"Worst track match: {1 - maxTrackDist:P1} vs {1 - _trackThreshold:P0} {trackReasons}"); - } - } } // otherwise importing existing files in library diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseTrackMatchSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseTrackMatchSpecification.cs deleted file mode 100644 index 11ffe237a..000000000 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseTrackMatchSpecification.cs +++ /dev/null @@ -1,33 +0,0 @@ -using NLog; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Download; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications -{ - public class CloseTrackMatchSpecification : IImportDecisionEngineSpecification - { - private const double _threshold = 0.4; - private readonly Logger _logger; - - public CloseTrackMatchSpecification(Logger logger) - { - _logger = logger; - } - - public Decision IsSatisfiedBy(LocalTrack item, DownloadClientItem downloadClientItem) - { - var dist = item.Distance.NormalizedDistance(); - var reasons = item.Distance.Reasons; - - if (dist > _threshold) - { - _logger.Debug($"Track match is not close enough: {dist} vs {_threshold} {reasons}. Skipping {item}"); - return Decision.Reject($"Track match is not close enough: {1 - dist:P1} vs {1 - _threshold:P0} {reasons}"); - } - - _logger.Debug($"Track accepted: {dist} vs {_threshold} {reasons}."); - return Decision.Accept(); - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs deleted file mode 100644 index 36454d3f2..000000000 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Linq; -using NLog; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Download; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications -{ - public class MoreTracksSpecification : IImportDecisionEngineSpecification - { - private readonly Logger _logger; - - public MoreTracksSpecification(Logger logger) - { - _logger = logger; - } - - public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) - { - var existingRelease = item.AlbumRelease.Album.Value.AlbumReleases.Value.Single(x => x.Monitored); - var existingTrackCount = existingRelease.Tracks.Value.Count(x => x.HasFile); - if (item.AlbumRelease.Id != existingRelease.Id && - item.TrackCount < existingTrackCount) - { - _logger.Debug($"This release has fewer tracks ({item.TrackCount}) than existing {existingRelease} ({existingTrackCount}). Skipping {item}"); - return Decision.Reject("Has fewer tracks than existing release"); - } - - _logger.Trace("Accepting release {0}", item); - return Decision.Accept(); - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs deleted file mode 100644 index d54fe89e9..000000000 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs +++ /dev/null @@ -1,34 +0,0 @@ -using NLog; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Download; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications -{ - public class NoMissingOrUnmatchedTracksSpecification : IImportDecisionEngineSpecification - { - private readonly Logger _logger; - - public NoMissingOrUnmatchedTracksSpecification(Logger logger) - { - _logger = logger; - } - - public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) - { - if (item.NewDownload && item.TrackMapping.LocalExtra.Count > 0) - { - _logger.Debug("This release has track files that have not been matched. Skipping {0}", item); - return Decision.Reject("Has unmatched tracks"); - } - - if (item.NewDownload && item.TrackMapping.MBExtra.Count > 0) - { - _logger.Debug("This release is missing tracks. Skipping {0}", item); - return Decision.Reject("Has missing tracks"); - } - - return Decision.Accept(); - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/ReleaseWantedSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/ReleaseWantedSpecification.cs deleted file mode 100644 index 269dd2082..000000000 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/ReleaseWantedSpecification.cs +++ /dev/null @@ -1,28 +0,0 @@ -using NLog; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.Download; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications -{ - public class ReleaseWantedSpecification : IImportDecisionEngineSpecification - { - private readonly Logger _logger; - - public ReleaseWantedSpecification(Logger logger) - { - _logger = logger; - } - - public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) - { - if (item.AlbumRelease.Monitored || item.AlbumRelease.Album.Value.AnyReleaseOk) - { - return Decision.Accept(); - } - - _logger.Debug("AlbumRelease {0} was not requested", item); - return Decision.Reject("Album release not requested"); - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameFileSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameFileSpecification.cs index 1453c4e2f..8bb252b2e 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameFileSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameFileSpecification.cs @@ -17,24 +17,21 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalTrack item, DownloadClientItem downloadClientItem) { - var trackFiles = item.Tracks.Where(e => e.TrackFileId != 0).Select(e => e.TrackFile).ToList(); + var trackFiles = item.Album?.BookFiles?.Value; - if (trackFiles.Count == 0) + if (trackFiles == null || !trackFiles.Any()) { _logger.Debug("No existing track file, skipping"); return Decision.Accept(); } - if (trackFiles.Count > 1) + foreach (var trackFile in trackFiles) { - _logger.Debug("More than one existing track file, skipping."); - return Decision.Accept(); - } - - if (trackFiles.First().Value.Size == item.Size) - { - _logger.Debug("'{0}' Has the same filesize as existing file", item.Path); - return Decision.Reject("Has the same filesize as existing file"); + if (trackFile.Size == item.Size) + { + _logger.Debug("'{0}' Has the same filesize as existing file", item.Path); + return Decision.Reject("Has the same filesize as existing file"); + } } return Decision.Accept(); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameTracksImportSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameTracksImportSpecification.cs deleted file mode 100644 index d51e390bc..000000000 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/SameTracksImportSpecification.cs +++ /dev/null @@ -1,33 +0,0 @@ -using NLog; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Download; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications -{ - public class SameTracksImportSpecification : IImportDecisionEngineSpecification - { - private readonly SameTracksSpecification _sameTracksSpecification; - private readonly Logger _logger; - - public SameTracksImportSpecification(SameTracksSpecification sameTracksSpecification, Logger logger) - { - _sameTracksSpecification = sameTracksSpecification; - _logger = logger; - } - - public RejectionType Type => RejectionType.Permanent; - - public Decision IsSatisfiedBy(LocalTrack item, DownloadClientItem downloadClientItem) - { - if (_sameTracksSpecification.IsSatisfiedBy(item.Tracks)) - { - return Decision.Accept(); - } - - _logger.Debug("Track file on disk contains more tracks than this file contains"); - return Decision.Reject("Track file on disk contains more tracks than this file contains"); - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs index d75d08ee8..4c7c70051 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs @@ -21,7 +21,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalTrack item, DownloadClientItem downloadClientItem) { - if (!item.Tracks.Any(e => e.TrackFileId > 0)) + var files = item.Album?.BookFiles?.Value; + if (files == null || !files.Any()) { // No existing tracks, skip. This guards against new artists not having a QualityProfile. return Decision.Accept(); @@ -30,16 +31,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks; var qualityComparer = new QualityModelComparer(item.Artist.QualityProfile); - foreach (var track in item.Tracks.Where(e => e.TrackFileId > 0)) + foreach (var trackFile in files) { - var trackFile = track.TrackFile.Value; - - if (trackFile == null) - { - _logger.Trace("Unable to get track file details from the DB. TrackId: {0} TrackFileId: {1}", track.Id, track.TrackFileId); - continue; - } - var qualityCompare = qualityComparer.Compare(item.Quality.Quality, trackFile.Quality.Quality); if (qualityCompare < 0) diff --git a/src/NzbDrone.Core/MediaFiles/UpdateTrackFileService.cs b/src/NzbDrone.Core/MediaFiles/UpdateTrackFileService.cs index d4aac29a0..b40862c62 100644 --- a/src/NzbDrone.Core/MediaFiles/UpdateTrackFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpdateTrackFileService.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.MediaFiles { public interface IUpdateTrackFileService { - void ChangeFileDateForFile(TrackFile trackFile, Artist artist, List tracks); + void ChangeFileDateForFile(BookFile trackFile, Author artist, Book book); } public class UpdateTrackFileService : IUpdateTrackFileService, @@ -23,29 +23,26 @@ namespace NzbDrone.Core.MediaFiles private readonly IDiskProvider _diskProvider; private readonly IAlbumService _albumService; private readonly IConfigService _configService; - private readonly ITrackService _trackService; private readonly Logger _logger; private static readonly DateTime EpochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); public UpdateTrackFileService(IDiskProvider diskProvider, - IConfigService configService, - ITrackService trackService, - IAlbumService albumService, - Logger logger) + IConfigService configService, + IAlbumService albumService, + Logger logger) { _diskProvider = diskProvider; _configService = configService; - _trackService = trackService; _albumService = albumService; _logger = logger; } - public void ChangeFileDateForFile(TrackFile trackFile, Artist artist, List tracks) + public void ChangeFileDateForFile(BookFile trackFile, Author artist, Book book) { - ChangeFileDate(trackFile, artist, tracks); + ChangeFileDate(trackFile, book); } - private bool ChangeFileDate(TrackFile trackFile, Artist artist, List tracks) + private bool ChangeFileDate(BookFile trackFile, Book album) { var trackFilePath = trackFile.Path; @@ -53,8 +50,6 @@ namespace NzbDrone.Core.MediaFiles { case FileDateType.AlbumReleaseDate: { - var album = _albumService.GetAlbum(trackFile.AlbumId); - if (!album.ReleaseDate.HasValue) { _logger.Debug("Could not create valid date to change file [{0}]", trackFilePath); @@ -64,7 +59,7 @@ namespace NzbDrone.Core.MediaFiles var relDate = album.ReleaseDate.Value; // avoiding false +ve checks and set date skewing by not using UTC (Windows) - DateTime oldDateTime = _diskProvider.FileGetLastWrite(trackFilePath); + var oldDateTime = _diskProvider.FileGetLastWrite(trackFilePath); if (OsInfo.IsNotWindows && relDate < EpochTime) { @@ -101,21 +96,21 @@ namespace NzbDrone.Core.MediaFiles return; } - var tracks = _trackService.TracksWithFiles(message.Artist.Id); + var books = _albumService.GetArtistAlbumsWithFiles(message.Artist); - var trackFiles = new List(); - var updated = new List(); + var trackFiles = new List(); + var updated = new List(); - foreach (var group in tracks.GroupBy(e => e.TrackFileId)) + foreach (var book in books) { - var tracksInFile = group.Select(e => e).ToList(); - var trackFile = tracksInFile.First().TrackFile; - - trackFiles.Add(trackFile); - - if (ChangeFileDate(trackFile, message.Artist, tracksInFile)) + var files = book.BookFiles.Value; + foreach (var file in files) { - updated.Add(trackFile); + trackFiles.Add(file); + if (ChangeFileDate(file, book)) + { + updated.Add(file); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index 0dd3c3e62..d5b403adb 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -1,15 +1,19 @@ +using System; +using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Books.Calibre; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.RootFolders; namespace NzbDrone.Core.MediaFiles { public interface IUpgradeMediaFiles { - TrackFileMoveResult UpgradeTrackFile(TrackFile trackFile, LocalTrack localTrack, bool copyOnly = false); + TrackFileMoveResult UpgradeTrackFile(BookFile trackFile, LocalTrack localTrack, bool copyOnly = false); } public class UpgradeMediaFileService : IUpgradeMediaFiles @@ -19,6 +23,9 @@ namespace NzbDrone.Core.MediaFiles private readonly IAudioTagService _audioTagService; private readonly IMoveTrackFiles _trackFileMover; private readonly IDiskProvider _diskProvider; + private readonly IRootFolderService _rootFolderService; + private readonly IRootFolderWatchingService _rootFolderWatchingService; + private readonly ICalibreProxy _calibre; private readonly Logger _logger; public UpgradeMediaFileService(IRecycleBinProvider recycleBinProvider, @@ -26,6 +33,9 @@ namespace NzbDrone.Core.MediaFiles IAudioTagService audioTagService, IMoveTrackFiles trackFileMover, IDiskProvider diskProvider, + IRootFolderService rootFolderService, + IRootFolderWatchingService rootFolderWatchingService, + ICalibreProxy calibre, Logger logger) { _recycleBinProvider = recycleBinProvider; @@ -33,55 +43,140 @@ namespace NzbDrone.Core.MediaFiles _audioTagService = audioTagService; _trackFileMover = trackFileMover; _diskProvider = diskProvider; + _rootFolderService = rootFolderService; + _rootFolderWatchingService = rootFolderWatchingService; + _calibre = calibre; _logger = logger; } - public TrackFileMoveResult UpgradeTrackFile(TrackFile trackFile, LocalTrack localTrack, bool copyOnly = false) + public TrackFileMoveResult UpgradeTrackFile(BookFile trackFile, LocalTrack localTrack, bool copyOnly = false) { var moveFileResult = new TrackFileMoveResult(); - var existingFiles = localTrack.Tracks - .Where(e => e.TrackFileId > 0) - .Select(e => e.TrackFile.Value) - .Where(e => e != null) - .GroupBy(e => e.Id) - .ToList(); + var existingFiles = localTrack.Album.BookFiles.Value; - var rootFolder = _diskProvider.GetParentFolder(localTrack.Artist.Path); + var rootFolderPath = _diskProvider.GetParentFolder(localTrack.Artist.Path); + var rootFolder = _rootFolderService.GetBestRootFolder(rootFolderPath); + var isCalibre = rootFolder.IsCalibreLibrary && rootFolder.CalibreSettings != null; + + var settings = rootFolder.CalibreSettings; // If there are existing track files and the root folder is missing, throw, so the old file isn't left behind during the import process. - if (existingFiles.Any() && !_diskProvider.FolderExists(rootFolder)) + if (existingFiles.Any() && !_diskProvider.FolderExists(rootFolderPath)) { - throw new RootFolderNotFoundException($"Root folder '{rootFolder}' was not found."); + throw new RootFolderNotFoundException($"Root folder '{rootFolderPath}' was not found."); } - foreach (var existingFile in existingFiles) + foreach (var file in existingFiles) { - var file = existingFile.First(); var trackFilePath = file.Path; - var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(trackFilePath)); + var subfolder = rootFolderPath.GetRelativePath(_diskProvider.GetParentFolder(trackFilePath)); + + trackFile.CalibreId = file.CalibreId; if (_diskProvider.FileExists(trackFilePath)) { _logger.Debug("Removing existing track file: {0}", file); - _recycleBinProvider.DeleteFile(trackFilePath, subfolder); + + if (!isCalibre) + { + _recycleBinProvider.DeleteFile(trackFilePath, subfolder); + } + else + { + _calibre.RemoveFormats(file.CalibreId, + new[] + { + Path.GetExtension(trackFile.Path) + }, + settings); + } } moveFileResult.OldFiles.Add(file); _mediaFileService.Delete(file, DeleteMediaFileReason.Upgrade); } - if (copyOnly) + if (!isCalibre) { - moveFileResult.TrackFile = _trackFileMover.CopyTrackFile(trackFile, localTrack); + if (copyOnly) + { + moveFileResult.TrackFile = _trackFileMover.CopyTrackFile(trackFile, localTrack); + } + else + { + moveFileResult.TrackFile = _trackFileMover.MoveTrackFile(trackFile, localTrack); + } + + _audioTagService.WriteTags(trackFile, true); } else { - moveFileResult.TrackFile = _trackFileMover.MoveTrackFile(trackFile, localTrack); - } + var source = trackFile.Path; - _audioTagService.WriteTags(trackFile, true); + moveFileResult.TrackFile = CalibreAddAndConvert(trackFile, settings); + + if (!copyOnly) + { + _diskProvider.DeleteFile(source); + } + } return moveFileResult; } + + public BookFile CalibreAddAndConvert(BookFile file, CalibreSettings settings) + { + _logger.Trace($"Importing to calibre: {file.Path}"); + + if (file.CalibreId == 0) + { + var import = _calibre.AddBook(file, settings); + file.CalibreId = import.Id; + } + else + { + _calibre.AddFormat(file, settings); + } + + _calibre.SetFields(file, settings); + + var updated = _calibre.GetBook(file.CalibreId, settings); + var path = updated.Formats.Values.OrderByDescending(x => x.LastModified).First().Path; + + file.Path = path; + + _rootFolderWatchingService.ReportFileSystemChangeBeginning(file.Path); + + if (settings.OutputFormat.IsNotNullOrWhiteSpace()) + { + _logger.Trace($"Getting book data for {file.CalibreId}"); + var options = _calibre.GetBookData(file.CalibreId, settings); + var inputFormat = file.Quality.Quality.Name.ToUpper(); + + options.Conversion_options.Input_fmt = inputFormat; + + var formats = settings.OutputFormat.Split(',').Select(x => x.Trim()); + foreach (var format in formats) + { + if (format.ToLower() == inputFormat || + options.Input_formats.Contains(format, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + options.Conversion_options.Output_fmt = format; + + if (settings.OutputProfile != (int)CalibreProfile.Default) + { + options.Conversion_options.Options.Output_profile = ((CalibreProfile)settings.OutputProfile).ToString(); + } + + _logger.Trace($"Starting conversion to {format}"); + _calibre.ConvertBook(file.CalibreId, options.Conversion_options, settings); + } + } + + return file; + } } } diff --git a/src/NzbDrone.Core/MetadataSource/IProvideAlbumInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideAlbumInfo.cs index 26cdfef24..ee7b72972 100644 --- a/src/NzbDrone.Core/MetadataSource/IProvideAlbumInfo.cs +++ b/src/NzbDrone.Core/MetadataSource/IProvideAlbumInfo.cs @@ -4,9 +4,9 @@ using NzbDrone.Core.Music; namespace NzbDrone.Core.MetadataSource { - public interface IProvideAlbumInfo + public interface IProvideBookInfo { - Tuple> GetAlbumInfo(string id); + Tuple> GetBookInfo(string id); HashSet GetChangedAlbums(DateTime startTime); } } diff --git a/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideAuthorInfo.cs similarity index 63% rename from src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs rename to src/NzbDrone.Core/MetadataSource/IProvideAuthorInfo.cs index f3761a350..7a1c6dd2b 100644 --- a/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs +++ b/src/NzbDrone.Core/MetadataSource/IProvideAuthorInfo.cs @@ -4,9 +4,9 @@ using NzbDrone.Core.Music; namespace NzbDrone.Core.MetadataSource { - public interface IProvideArtistInfo + public interface IProvideAuthorInfo { - Artist GetArtistInfo(string readarrId, int metadataProfileId); + Author GetAuthorInfo(string readarrId); HashSet GetChangedArtists(DateTime startTime); } } diff --git a/src/NzbDrone.Core/MetadataSource/ISearchForNewAlbum.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewAlbum.cs index 1326efb8c..455650d41 100644 --- a/src/NzbDrone.Core/MetadataSource/ISearchForNewAlbum.cs +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewAlbum.cs @@ -3,9 +3,12 @@ using NzbDrone.Core.Music; namespace NzbDrone.Core.MetadataSource { - public interface ISearchForNewAlbum + public interface ISearchForNewBook { - List SearchForNewAlbum(string title, string artist); - List SearchForNewAlbumByRecordingIds(List recordingIds); + List SearchForNewBook(string title, string artist); + List SearchByIsbn(string isbn); + List SearchByAsin(string asin); + List SearchByGoodreadsId(int goodreadsId); + List SearchForNewAlbumByRecordingIds(List recordingIds); } } diff --git a/src/NzbDrone.Core/MetadataSource/ISearchForNewArtist.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewArtist.cs index 5a593712a..3bdf52728 100644 --- a/src/NzbDrone.Core/MetadataSource/ISearchForNewArtist.cs +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewArtist.cs @@ -3,8 +3,8 @@ using NzbDrone.Core.Music; namespace NzbDrone.Core.MetadataSource { - public interface ISearchForNewArtist + public interface ISearchForNewAuthor { - List SearchForNewArtist(string title); + List SearchForNewAuthor(string title); } } diff --git a/src/NzbDrone.Core/MetadataSource/SearchArtistComparer.cs b/src/NzbDrone.Core/MetadataSource/SearchArtistComparer.cs index 13a6f8201..8c7978029 100644 --- a/src/NzbDrone.Core/MetadataSource/SearchArtistComparer.cs +++ b/src/NzbDrone.Core/MetadataSource/SearchArtistComparer.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Music; namespace NzbDrone.Core.MetadataSource { - public class SearchArtistComparer : IComparer + public class SearchArtistComparer : IComparer { private static readonly Regex RegexCleanPunctuation = new Regex("[-._:]", RegexOptions.Compiled); private static readonly Regex RegexCleanCountryYearPostfix = new Regex(@"(?<=.+)( \([A-Z]{2}\)| \(\d{4}\)| \([A-Z]{2}\) \(\d{4}\))$", RegexOptions.Compiled); @@ -33,7 +33,7 @@ namespace NzbDrone.Core.MetadataSource } } - public int Compare(Artist x, Artist y) + public int Compare(Author x, Author y) { int result = 0; @@ -61,7 +61,7 @@ namespace NzbDrone.Core.MetadataSource return Compare(x, y, s => SearchQuery.LevenshteinDistanceClean(s.Name)); } - public int Compare(Artist x, Artist y, Func keySelector) + public int Compare(Author x, Author y, Func keySelector) where T : IComparable { var keyX = keySelector(x); diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Extensions/HttpResponseExtensions.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Extensions/HttpResponseExtensions.cs new file mode 100644 index 000000000..01c1febd7 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Extensions/HttpResponseExtensions.cs @@ -0,0 +1,117 @@ +using System; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +using NzbDrone.Common.Http; +using NzbDrone.Core.MetadataSource.SkyHook; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + public static class HttpResponseExtensions + { + public static T Deserialize(this HttpResponse response, string elementName = null) + where T : GoodreadsResource, new() + { + response.ThrowIfException(); + + try + { + var document = XDocument.Parse(response.Content); + if (document.Root == null || + document.Root.Name == "error") + { + return null; + } + else + { + var root = document.Element("GoodreadsResponse") ?? (XNode)document; + var responseObject = new T(); + var contentRoot = root.XPathSelectElement(elementName ?? responseObject.ElementName); + + responseObject.Parse(contentRoot); + return responseObject; + } + } + catch (XmlException) + { + return null; + } + } + + private static void ThrowIfException(this HttpResponse response) + { + // Try and find an error from the Goodreads response + string error = null; + try + { + var document = XDocument.Parse(response.Content); + + // Goodreads returns several different types of errors... + if (document.Root != null) + { + if (document.Root.Name == "error") + { + // One is a single XML error node + var element = document.Element("error"); + if (element != null) + { + error = element.Value; + } + } + else if (document.Root.Name == "errors") + { + // Another one is a list of XML error nodes + var element = document.Element("errors"); + var children = element?.Descendants("error"); + if (children.Any()) + { + error = string.Join(Environment.NewLine, children.Select(x => x.Value)); + } + } + else if (document.Root.Name == "hash") + { + // And another one is in a "hash" XML object + var element = document.Element("hash"); + if (element != null) + { + var status = element.ElementAsString("status"); + var message = element.ElementAsString("error"); + if (!string.IsNullOrEmpty(message)) + { + error = string.Join(" ", status, message); + } + } + } + else + { + // Yet another one is an entire XML structure with multiple messages... + var element = document.XPathSelectElement("GoodreadsResponse/error"); + if (element != null) + { + // There are four total error messages + var plain = element.Value; + var genericMessage = element.ElementAsString("generic"); + var detailMessage = element.ElementAsString("detail"); + var friendlyMessage = element.ElementAsString("friendly"); + + // Use the best message that exists... + error = friendlyMessage ?? detailMessage ?? genericMessage ?? plain; + } + } + } + } + catch (XmlException) + { + // We don't really care if any exception was thrown above + // we're just trying to find an error message after all... + } + + // If we found any error at all above, throw an exception + if (!string.IsNullOrWhiteSpace(error)) + { + throw new SkyHookException("Received an error from Goodreads " + error); + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Extensions/XmlExtensions.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Extensions/XmlExtensions.cs new file mode 100644 index 000000000..6fb2f10fe --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Extensions/XmlExtensions.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + internal static class XmlExtensions + { + public static string ElementAsString(this XElement element, XName name, bool trim = false) + { + var el = element.Element(name); + + return string.IsNullOrWhiteSpace(el?.Value) + ? null + : (trim ? el.Value.Trim() : el.Value); + } + + public static long ElementAsLong(this XElement element, XName name) + { + var el = element.Element(name); + return long.TryParse(el?.Value, out long value) ? value : default(long); + } + + public static long? ElementAsNullableLong(this XElement element, XName name) + { + var el = element.Element(name); + return long.TryParse(el?.Value, out long value) ? new long?(value) : null; + } + + public static int ElementAsInt(this XElement element, XName name) + { + var el = element.Element(name); + return int.TryParse(el?.Value, out int value) ? value : default(int); + } + + public static int? ElementAsNullableInt(this XElement element, XName name) + { + var el = element.Element(name); + return int.TryParse(el?.Value, out int value) ? new int?(value) : null; + } + + public static decimal ElementAsDecimal(this XElement element, XName name) + { + var el = element.Element(name); + return decimal.TryParse(el?.Value, out decimal value) ? value : default(decimal); + } + + public static decimal? ElementAsNullableDecimal(this XElement element, XName name) + { + var el = element.Element(name); + return decimal.TryParse(el?.Value, out decimal value) ? new decimal?(value) : null; + } + + public static DateTime? ElementAsDate(this XElement element, XName name) + { + var el = element.Element(name); + return DateTime.TryParseExact(el?.Value, "yyyy/MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime date) + ? new DateTime?(date) + : null; + } + + public static DateTime? ElementAsDateTime(this XElement element, XName name) + { + var dateElement = element.Element(name); + if (dateElement != null) + { + var value = dateElement.Value; + + // The Goodreads date includes the timezone as -hhmm whereas C# wants it to be -hh:mm + // This regex corrects the format and hopefully doesn't mess anything else up... + var validDateFormat = Regex.Replace(value, @"(.*) ([+-]\d{2})(\d{2}) (.*)", "$1 $2:$3 $4"); + + DateTime localDate; + if (DateTime.TryParseExact( + validDateFormat, + "ddd MMM dd HH:mm:ss zzz yyyy", + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out localDate)) + { + return localDate.ToUniversalTime(); + } + else if (DateTime.TryParseExact( + validDateFormat, + "yyyy-MM-ddTHH:mm:sszzz", + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out localDate)) + { + return localDate.ToUniversalTime(); + } + } + + return null; + } + + public static DateTime? ElementAsMonthYear(this XElement element, XName name) + { + var el = element.Element(name); + return DateTime.TryParseExact(el?.Value, "MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime date) + ? new DateTime?(date) + : null; + } + + /// + /// Goodreads sometimes returns dates as three separate fields. + /// This method parses out each one and returns a date object. + /// + /// The parent element of the date elements. + /// The common prefix for the three Goodreads date elements. + /// A date object after parsing the three Goodreads date fields. + public static DateTime? ElementAsMultiDateField(this XElement element, string prefix) + { + var publicationYear = element.ElementAsNullableInt(prefix + "_year"); + var publicationMonth = element.ElementAsNullableInt(prefix + "_month"); + var publicationDay = element.ElementAsNullableInt(prefix + "_day"); + + if (!publicationYear.HasValue && + !publicationMonth.HasValue && + !publicationDay.HasValue) + { + return null; + } + + if (!publicationYear.HasValue) + { + return null; + } + + if (!publicationDay.HasValue) + { + publicationDay = 1; + } + + if (!publicationMonth.HasValue) + { + publicationMonth = 1; + } + + try + { + return new DateTime(publicationYear.Value, publicationMonth.Value, publicationDay.Value); + } + catch + { + return null; + } + } + + public static bool ElementAsBool(this XElement element, XName name) + { + var el = element.Element(name); + return bool.TryParse(el?.Value, out bool value) ? value : false; + } + + public static List ParseChildren(this XElement element, XName parentName, XName childName) + where T : GoodreadsResource, new() + { + return ParseChildren( + element, + parentName, + childName, + (childElement) => + { + var child = new T(); + child.Parse(childElement); + return child; + }); + } + + public static List ParseChildren(this XElement element, XName parentName, XName childName, Func parseChild) + { + var parentElement = element.Element(parentName); + if (parentElement != null) + { + var childElements = parentElement.Descendants(childName); + if (childElements.Any()) + { + var children = new List(); + + foreach (var childElement in childElements) + { + children.Add(parseChild(childElement)); + } + + return children; + } + } + + return null; + } + + public static List ParseChildren(this XElement element) + where T : GoodreadsResource, new() + { + var childElements = element.Elements(); + if (childElements.Any()) + { + var children = new List(); + + foreach (var childElement in childElements) + { + var child = new T(); + child.Parse(childElement); + children.Add(child); + } + + return children; + } + + return null; + } + + public static string AttributeAsString(this XElement element, XName attributeName) + { + var attr = element.Attribute(attributeName); + return string.IsNullOrWhiteSpace(attr?.Value) ? null : attr.Value; + } + + public static int AttributeAsInt(this XElement element, XName attributeName) + { + var attr = element.Attribute(attributeName); + return int.TryParse(attr?.Value, out int value) ? value : default(int); + } + + public static long? AttributeAsNullableLong(this XElement element, XName attributeName) + { + var attr = element.Attribute(attributeName); + return long.TryParse(attr?.Value, out long value) ? new long?(value) : null; + } + + public static bool AttributeAsBool(this XElement element, XName attributeName) + { + var attr = element.Attribute(attributeName); + return bool.TryParse(attr?.Value, out bool value) ? value : false; + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorBookListResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorBookListResource.cs new file mode 100644 index 000000000..e2ac7ccea --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorBookListResource.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models the best book in a work, as defined by the Goodreads API. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class AuthorBookListResource : GoodreadsResource + { + public override string ElementName => "author"; + + public List List { get; private set; } + + public override void Parse(XElement element) + { + var results = element.Descendants("books"); + if (results.Count() == 1) + { + List = results.First().ParseChildren(); + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorResource.cs new file mode 100644 index 000000000..b74f64070 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorResource.cs @@ -0,0 +1,145 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models an Author as defined by the Goodreads API. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class AuthorResource : GoodreadsResource + { + public override string ElementName => "author"; + + /// + /// The Goodreads Author Id. + /// + public long Id { get; private set; } + + /// + /// The full name of the author. + /// + public string Name { get; private set; } + + /// + /// The Url to the Goodreads author page. + /// + public string Link { get; private set; } + + /// + /// The number of fans for this author. + /// The Goodreads Fan API has been replaced by Followers. + /// For this property, use FollowersCount instead. + /// + [Obsolete("Fans API has been deprecated by Goodreads. Use Followers instead.")] + public int FansCount { get; private set; } + + /// + /// The number of Goodreads users that are following this author. + /// + public int FollowersCount { get; private set; } + + /// + /// The Url to the author's image, large size. + /// + public string LargeImageUrl { get; private set; } + + /// + /// The Url to the author's image. + /// + public string ImageUrl { get; private set; } + + /// + /// The Url to the author's image, small size. + /// + public string SmallImageUrl { get; private set; } + + /// + /// A brief description about this author. This field may contain HTML. + /// + public string About { get; private set; } + + /// + /// People that may have influenced this author. This field may contain HTML. + /// + public string Influences { get; private set; } + + /// + /// The total number of items the author has worked on and are listed within Goodreads. + /// + public int WorksCount { get; private set; } + + /// + /// The gender of the author. This field might be limited to only "male" and "female" + /// but is left as a string in case any other options are possible through the Goodreads API. + /// + public string Gender { get; private set; } + + /// + /// The hometown the author grew up in. + /// + public string Hometown { get; private set; } + + /// + /// The author's birthdate. + /// + public DateTime? BornOnDate { get; private set; } + + /// + /// The date on which the author died. + /// + public DateTime? DiedOnDate { get; private set; } + + /// + /// Determines whether this author is also a regular Goodreads user or not. + /// + public bool IsGoodreadsAuthor { get; private set; } + + /// + /// If is true, this property is set to the author's Goodreads user Id. + /// + public int? GoodreadsUserId { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format( + CultureInfo.InvariantCulture, + "Author: Id: {0}, Name: {1}", + Id, + Name); + } + } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + Name = element.ElementAsString("name"); + Link = element.ElementAsString("link"); + FollowersCount = element.ElementAsInt("author_followers_count"); + LargeImageUrl = element.ElementAsString("large_image_url"); + ImageUrl = element.ElementAsString("image_url"); + SmallImageUrl = element.ElementAsString("small_image_url"); + About = element.ElementAsString("about"); + Influences = element.ElementAsString("influences"); + WorksCount = element.ElementAsInt("works_count"); + Gender = element.ElementAsString("gender"); + Hometown = element.ElementAsString("hometown"); + BornOnDate = element.ElementAsDate("born_at"); + DiedOnDate = element.ElementAsDate("died_at"); + + IsGoodreadsAuthor = element.ElementAsBool("goodreads_author"); + if (IsGoodreadsAuthor) + { + var goodreadsUser = element.Element("user"); + if (goodreadsUser != null) + { + GoodreadsUserId = goodreadsUser.ElementAsInt("id"); + } + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorSeriesListResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorSeriesListResource.cs new file mode 100644 index 000000000..911f4b95c --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorSeriesListResource.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models the best book in a work, as defined by the Goodreads API. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class AuthorSeriesListResource : GoodreadsResource + { + public override string ElementName => "series_works"; + + public List List { get; private set; } + + public override void Parse(XElement element) + { + var pairs = element.Descendants("series_work"); + if (pairs.Any()) + { + var dict = new Dictionary(); + + foreach (var pair in pairs) + { + var series = new SeriesResource(); + series.Parse(pair.Element("series")); + + if (!dict.TryGetValue(series.Id, out var cached)) + { + dict[series.Id] = series; + cached = series; + } + + var work = new WorkResource(); + work.Parse(pair.Element("work")); + work.SetSeriesInfo(pair); + + cached.Works.Add(work); + } + + List = dict.Values.ToList(); + } + else + { + List = new List(); + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorSummaryResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorSummaryResource.cs new file mode 100644 index 000000000..cff5da3aa --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/AuthorSummaryResource.cs @@ -0,0 +1,73 @@ +using System.Diagnostics; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models areas of the API where Goodreads returns + /// very brief information about an Author instead of their entire profile. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class AuthorSummaryResource : GoodreadsResource + { + public override string ElementName => "author"; + + /// + /// The Goodreads Author Id. + /// + public long Id { get; private set; } + + /// + /// The name of this author. + /// + public string Name { get; private set; } + + /// + /// The role of this author. + /// + public string Role { get; private set; } + + /// + /// The image of this author, regular size. + /// + public string ImageUrl { get; private set; } + + /// + /// The image of this author, small size. + /// + public string SmallImageUrl { get; private set; } + + /// + /// The link to the Goodreads page for this author. + /// + public string Link { get; private set; } + + /// + /// The average rating for all of this author's books. + /// + public decimal? AverageRating { get; private set; } + + /// + /// The total count of all ratings of this author's books. + /// + public int? RatingsCount { get; private set; } + + /// + /// The total count of all the text reviews of this author's books. + /// + public int? TextReviewsCount { get; private set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + Name = element.ElementAsString("name"); + Role = element.ElementAsString("role"); + ImageUrl = element.ElementAsString("image_url"); + SmallImageUrl = element.ElementAsString("small_image_url"); + Link = element.ElementAsString("link"); + AverageRating = element.ElementAsNullableDecimal("average_rating"); + RatingsCount = element.ElementAsNullableInt("ratings_count"); + TextReviewsCount = element.ElementAsNullableInt("text_reviews_count"); + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BestBookResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BestBookResource.cs new file mode 100644 index 000000000..54cfee1a4 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BestBookResource.cs @@ -0,0 +1,57 @@ +using System.Diagnostics; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models the best book in a work, as defined by the Goodreads API. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class BestBookResource : GoodreadsResource + { + public override string ElementName => "best_book"; + + /// + /// The Id of this book. + /// + public long Id { get; private set; } + + /// + /// The title of this book. + /// + public string Title { get; private set; } + + /// + /// The Goodreads id of the author. + /// + public long AuthorId { get; private set; } + + /// + /// The name of the author. + /// + public string AuthorName { get; private set; } + + /// + /// The cover image of this book. + /// + public string ImageUrl { get; private set; } + + public string LargeImageUrl { get; private set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + Title = element.ElementAsString("title"); + + var authorElement = element.Element("author"); + if (authorElement != null) + { + AuthorId = authorElement.ElementAsLong("id"); + AuthorName = authorElement.ElementAsString("name"); + } + + ImageUrl = element.ElementAsString("image_url"); + ImageUrl = element.ElementAsString("large_image_url"); + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookLinkResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookLinkResource.cs new file mode 100644 index 000000000..68e307a45 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookLinkResource.cs @@ -0,0 +1,56 @@ +using System.Diagnostics; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models a book link as defined by the Goodreads API. + /// This is usually a link to a third-party site to purchase the book. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class BookLinkResource : GoodreadsResource + { + public override string ElementName => "book_link"; + + /// + /// The Id of this book link. + /// + public long Id { get; private set; } + + /// + /// The name of this book link provider. + /// + public string Name { get; private set; } + + /// + /// The link to this book on the provider's site. + /// Be sure to append book_id as a query parameter + /// to actually be redirected to the correct page. + /// + public string Link { get; private set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + Name = element.ElementAsString("name"); + Link = element.ElementAsString("link"); + } + + /// + /// Goodreads returns incomplete book links for some reason. + /// The link results in an error unless you append a book_id query parameter. + /// This method fixes up these book links with the given book id. + /// + /// The book id to append to the book link. + internal void FixBookLink(long bookId) + { + if (!string.IsNullOrWhiteSpace(Link)) + { + if (!Link.Contains("book_id")) + { + Link += (Link.Contains("?") ? "&" : "?") + "book_id=" + bookId; + } + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookResource.cs new file mode 100644 index 000000000..245a4185c --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookResource.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models a single book as defined by the Goodreads API. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class BookResource : GoodreadsResource + { + public override string ElementName => "book"; + + /// + /// The Goodreads Id for this book. + /// + public long Id { get; private set; } + + /// + /// The title of this book. + /// + public string Title { get; private set; } + + /// + /// The description of this book. + /// + public string Description { get; private set; } + + /// + /// The ISBN of this book. + /// + public string Isbn { get; private set; } + + /// + /// The ISBN13 of this book. + /// + public string Isbn13 { get; private set; } + + /// + /// The ASIN of this book. + /// + public string Asin { get; private set; } + + /// + /// The Kindle ASIN of this book. + /// + public string KindleAsin { get; private set; } + + /// + /// The marketplace Id of this book. + /// + public string MarketplaceId { get; private set; } + + /// + /// The country code of this book. + /// + public string CountryCode { get; private set; } + + /// + /// The cover image for this book. + /// + public string ImageUrl { get; private set; } + + /// + /// The small cover image for this book. + /// + public string SmallImageUrl { get; private set; } + + /// + /// The date this book was published. + /// + public DateTime? PublicationDate { get; private set; } + + /// + /// The publisher of this book. + /// + public string Publisher { get; private set; } + + /// + /// The language code of this book. + /// + public string LanguageCode { get; private set; } + + /// + /// Signifies if this is an eBook or not. + /// + public bool IsEbook { get; private set; } + + /// + /// The average rating of this book by Goodreads users. + /// + public decimal AverageRating { get; private set; } + + /// + /// The number of pages in this book. + /// + public int Pages { get; private set; } + + /// + /// The format of this book. + /// + public string Format { get; private set; } + + /// + /// Brief information about this edition of the book. + /// + public string EditionInformation { get; private set; } + + /// + /// The count of all Goodreads ratings for this book. + /// + public int RatingsCount { get; private set; } + + /// + /// The count of all reviews that contain text for this book. + /// + public int TextReviewsCount { get; private set; } + + /// + /// The Goodreads Url for this book. + /// + public string Url { get; private set; } + + /// + /// The aggregate information for this work across all editions of the book. + /// + public WorkResource Work { get; private set; } + + /// + /// The list of authors that worked on this book. + /// + public IReadOnlyList Authors { get; private set; } + + /// + /// HTML and CSS for the Goodreads iFrame. Used to display the reviews for this book. + /// + public string ReviewsWidget { get; private set; } + + /// + /// The most popular shelf names this book appears on. This is a + /// dictionary of shelf name -> count. + /// + public IReadOnlyDictionary PopularShelves { get; private set; } + + /// + /// The list of book links tracked by Goodreads. + /// This is usually a list of libraries that the user can borrow the book from. + /// + public IReadOnlyList BookLinks { get; private set; } + + /// + /// The list of buy links tracked by Goodreads. + /// This is usually a list of third-party sites that the + /// user can purchase the book from. + /// + public IReadOnlyList BuyLinks { get; private set; } + + /// + /// Summary information about similar books to this one. + /// + public IReadOnlyList SimilarBooks { get; private set; } + + // TODO: parse series information once I get a better sense + // of what series are from the other API calls. + //// public List Series { get; private set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + Title = element.ElementAsString("title"); + Isbn = element.ElementAsString("isbn"); + Isbn13 = element.ElementAsString("isbn13"); + Asin = element.ElementAsString("asin"); + KindleAsin = element.ElementAsString("kindle_asin"); + MarketplaceId = element.ElementAsString("marketplace_id"); + CountryCode = element.ElementAsString("country_code"); + ImageUrl = element.ElementAsString("image_url"); + SmallImageUrl = element.ElementAsString("small_image_url"); + PublicationDate = element.ElementAsMultiDateField("publication"); + Publisher = element.ElementAsString("publisher"); + LanguageCode = element.ElementAsString("language_code"); + IsEbook = element.ElementAsBool("is_ebook"); + Description = element.ElementAsString("description"); + AverageRating = element.ElementAsDecimal("average_rating"); + Pages = element.ElementAsInt("num_pages"); + Format = element.ElementAsString("format"); + EditionInformation = element.ElementAsString("edition_information"); + RatingsCount = element.ElementAsInt("ratings_count"); + TextReviewsCount = element.ElementAsInt("text_reviews_count"); + Url = element.ElementAsString("url"); + ReviewsWidget = element.ElementAsString("reviews_widget"); + + var workElement = element.Element("work"); + if (workElement != null) + { + Work = new WorkResource(); + Work.Parse(workElement); + } + + Authors = element.ParseChildren("authors", "author"); + SimilarBooks = element.ParseChildren("similar_books", "book"); + + var bookLinks = element.ParseChildren("book_links", "book_link"); + if (bookLinks != null) + { + bookLinks.ForEach(x => x.FixBookLink(Id)); + BookLinks = bookLinks; + } + + var buyLinks = element.ParseChildren("buy_links", "buy_link"); + if (buyLinks != null) + { + buyLinks.ForEach(x => x.FixBookLink(Id)); + BuyLinks = buyLinks; + } + + var shelves = element.ParseChildren( + "popular_shelves", + "shelf", + (shelfElement) => + { + var shelfName = shelfElement?.Attribute("name")?.Value; + var shelfCountValue = shelfElement?.Attribute("count")?.Value; + + int shelfCount = 0; + int.TryParse(shelfCountValue, out shelfCount); + return new KeyValuePair(shelfName, shelfCount); + }); + + if (shelves != null) + { + PopularShelves = shelves.GroupBy(obj => obj.Key).ToDictionary(shelf => shelf.Key, shelf => shelf.Sum(x => x.Value)); + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookSearchResultResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookSearchResultResource.cs new file mode 100644 index 000000000..6f13a08c5 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookSearchResultResource.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models the best book in a work, as defined by the Goodreads API. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class BookSearchResultResource : GoodreadsResource + { + public override string ElementName => "search"; + + public List Results { get; private set; } + + public override void Parse(XElement element) + { + var results = element.Descendants("results"); + if (results.Count() == 1) + { + Results = results.First().ParseChildren(); + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookSummaryResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookSummaryResource.cs new file mode 100644 index 000000000..6176ef73c --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/BookSummaryResource.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models areas of the API where Goodreads returns + /// very brief information about a Book instead of their entire object. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class BookSummaryResource : GoodreadsResource + { + public override string ElementName => "book"; + + /// + /// The Id of this book. + /// + public long Id { get; private set; } + + public string Uri { get; set; } + + /// + /// The title of this book. + /// + public string Title { get; private set; } + + /// + /// The title of this book without series information in it. + /// + public string TitleWithoutSeries { get; private set; } + + /// + /// The link to the Goodreads page for this book. + /// + public string Link { get; private set; } + + /// + /// The cover image of this book, regular size. + /// + public string ImageUrl { get; private set; } + + /// + /// The cover image of this book, small size. + /// + public string SmallImageUrl { get; private set; } + + /// + /// The work id of this book. + /// + public long? WorkId { get; private set; } + + /// + /// The ISBN of this book. + /// + public string Isbn { get; private set; } + + /// + /// The ISBN13 of this book. + /// + public string Isbn13 { get; private set; } + + /// + /// The average rating of the book. + /// + public decimal? AverageRating { get; private set; } + + /// + /// The count of all ratings for the book. + /// + public int? RatingsCount { get; private set; } + + /// + /// The date this book was published. + /// + public DateTime? PublicationDate { get; private set; } + + /// + /// Summary information about the authors of this book. + /// + public IReadOnlyList Authors { get; private set; } + + /// + /// The edition information about book. + /// + public string EditionInformation { get; private set; } + + /// + /// The book format. + /// + public string Format { get; private set; } + + /// + /// The book description. + /// + public string Description { get; private set; } + + /// + /// Number of pages. + /// + public int NumberOfPages { get; private set; } + + /// + /// The book publisher. + /// + public string Publisher { get; private set; } + + /// + /// The image url, large size. + /// + public string LargeImageUrl { get; private set; } + + /// + /// A count of text reviews for this book. + /// + public int TextReviewsCount { get; private set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + Uri = element.ElementAsString("uri"); + Title = element.ElementAsString("title"); + TitleWithoutSeries = element.ElementAsString("title_without_series"); + Link = element.ElementAsString("link"); + ImageUrl = element.ElementAsString("image_url"); + SmallImageUrl = element.ElementAsString("small_image_url"); + Isbn = element.ElementAsString("isbn"); + Isbn13 = element.ElementAsString("isbn13"); + AverageRating = element.ElementAsNullableDecimal("average_rating"); + RatingsCount = element.ElementAsNullableInt("ratings_count"); + PublicationDate = element.ElementAsMultiDateField("publication"); + Authors = element.ParseChildren("authors", "author"); + + var workElement = element.Element("work"); + if (workElement != null) + { + WorkId = workElement.ElementAsNullableInt("id"); + } + + EditionInformation = element.ElementAsString("edition_information"); + Format = element.ElementAsString("format"); + Description = element.ElementAsString("description"); + NumberOfPages = element.ElementAsInt("num_pages"); + Publisher = element.ElementAsString("publisher"); + LargeImageUrl = element.ElementAsString("large_image_url"); + TextReviewsCount = element.ElementAsInt("text_reviews_count"); + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/GoodreadsResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/GoodreadsResource.cs new file mode 100644 index 000000000..d9929bde9 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/GoodreadsResource.cs @@ -0,0 +1,11 @@ +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + public abstract class GoodreadsResource + { + public abstract string ElementName { get; } + + public abstract void Parse(XElement element); + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/OwnedBookResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/OwnedBookResource.cs new file mode 100644 index 000000000..78879e8b3 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/OwnedBookResource.cs @@ -0,0 +1,82 @@ +using System; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models areas of the API where Goodreads returns + /// information about an user owned books. + /// + public sealed class OwnedBookResource : GoodreadsResource + { + public override string ElementName => "owned_book"; + + /// + /// The owner book id. + /// + public long Id { get; private set; } + + /// + /// The owner id. + /// + public long OwnerId { get; private set; } + + /// + /// The original date when owner has bought a book. + /// + public DateTime? OriginalPurchaseDate { get; private set; } + + /// + /// The original location where owner has bought a book. + /// + public string OriginalPurchaseLocation { get; private set; } + + /// + /// The owned book condition. + /// + public string Condition { get; private set; } + + /// + /// The traded count. + /// + public int TradedCount { get; private set; } + + /// + /// The link to the owned book. + /// + public string Link { get; private set; } + + /// + /// The book. + /// + public BookSummaryResource Book { get; private set; } + + /// + /// The owned book review. + /// + public ReviewResource Review { get; private set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + OwnerId = element.ElementAsLong("current_owner_id"); + OriginalPurchaseDate = element.ElementAsDateTime("original_purchase_date"); + OriginalPurchaseLocation = element.ElementAsString("original_purchase_location"); + Condition = element.ElementAsString("condition"); + + var review = element.Element("review"); + if (review != null) + { + Review = new ReviewResource(); + Review.Parse(review); + } + + var book = element.Element("book"); + if (book != null) + { + Book = new BookSummaryResource(); + Book.Parse(book); + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/PaginatedList.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/PaginatedList.cs new file mode 100644 index 000000000..f136a138c --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/PaginatedList.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// Represents a paginated list of objects as returned by the Goodreads API, + /// along with pagination information about the page size, current page, etc... + /// + /// The type of the object in the paginated list. + public class PaginatedList : GoodreadsResource + where T : GoodreadsResource, new() + { + public override string ElementName => ""; + + /// + /// The list of objects for the current page. + /// + public IReadOnlyList List { get; private set; } + + /// + /// Pagination information about the list and current page. + /// + public PaginationModel Pagination { get; private set; } + + public override void Parse(XElement element) + { + Pagination = new PaginationModel(); + Pagination.Parse(element); + + // Should have known search pagination would be different... + if (element.Name == "search") + { + var results = element.Descendants("results"); + if (results.Count() == 1) + { + List = results.First().ParseChildren(); + } + } + else + { + List = element.ParseChildren(); + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/PaginationModel.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/PaginationModel.cs new file mode 100644 index 000000000..f93ec2e06 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/PaginationModel.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// Represents pagination information as returned by the Goodreads API. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class PaginationModel : GoodreadsResource + { + public override string ElementName => ""; + + /// + /// The item the current page starts on. + /// + public int Start { get; private set; } + + /// + /// The item the current page ends on. + /// + public int End { get; private set; } + + /// + /// The total number of items in the paginated list. + /// + public int TotalItems { get; private set; } + + public override void Parse(XElement element) + { + // Search results have different pagination fields for some reason... + if (element.Name == "search") + { + Start = element.ElementAsInt("results-start"); + End = element.ElementAsInt("results-end"); + TotalItems = element.ElementAsInt("total-results"); + return; + } + + var startAttribute = element.Attribute("start"); + var endAttribute = element.Attribute("end"); + var totalAttribute = element.Attribute("total"); + + if (startAttribute != null && + endAttribute != null && + totalAttribute != null) + { + int.TryParse(startAttribute.Value, out int start); + int.TryParse(endAttribute.Value, out int end); + int.TryParse(totalAttribute.Value, out int total); + + Start = start; + End = end; + TotalItems = total; + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/ReviewResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/ReviewResource.cs new file mode 100644 index 000000000..04cbc20dd --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/ReviewResource.cs @@ -0,0 +1,131 @@ +using System; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models a Review as defined by the Goodreads API. + /// + public class ReviewResource : GoodreadsResource + { + public override string ElementName => "review"; + + /// + /// The Goodreads review id. + /// + public long Id { get; protected set; } + + /// + /// The summary information for the book this review is for. + /// + public BookSummaryResource Book { get; protected set; } + + /// + /// The rating the user gave the book in this review. + /// + public int Rating { get; protected set; } + + /// + /// The number of votes this review received from other Goodreads users. + /// + public int Votes { get; protected set; } + + /// + /// A flag determining if the review contains spoilers. + /// + public bool IsSpoiler { get; protected set; } + + /// + /// The state of the spoilers for this review. + /// + public string SpoilersState { get; protected set; } + + /// + /// The shelves the user has added this review to. + /// + // public IReadOnlyList Shelves { get; protected set; } + + /// + /// Who the user would recommend reading this book. + /// + public string RecommendedFor { get; protected set; } + + /// + /// Who recommended the user to read this book. + /// + public string RecommendedBy { get; protected set; } + + /// + /// The date the user started reading this book. + /// + public DateTime? DateStarted { get; protected set; } + + /// + /// The date the user finished reading this book. + /// + public DateTime? DateRead { get; protected set; } + + /// + /// The date the user added this book to their shelves. + /// + public DateTime? DateAdded { get; protected set; } + + /// + /// The date the user last updated this book on their shelves. + /// + public DateTime? DateUpdated { get; protected set; } + + /// + /// The number of times this book has been read. + /// + public int? ReadCount { get; protected set; } + + /// + /// The main text of this review. May contain HTML. + /// + public string Body { get; protected set; } + + /// + /// The number of comments on this review. + /// + public int CommentsCount { get; protected set; } + + /// + /// The Goodreads URL of this review. + /// + public string Url { get; protected set; } + + /// + /// The owned count of the book. + /// + public int Owned { get; protected set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + + var bookElement = element.Element("book"); + if (bookElement != null) + { + Book = new BookSummaryResource(); + Book.Parse(bookElement); + } + + Rating = element.ElementAsInt("rating"); + Votes = element.ElementAsInt("votes"); + IsSpoiler = element.ElementAsBool("spoiler_flag"); + SpoilersState = element.ElementAsString("spoilers_state"); + RecommendedFor = element.ElementAsString("recommended_for"); + RecommendedBy = element.ElementAsString("recommended_by"); + DateStarted = element.ElementAsDateTime("started_at"); + DateRead = element.ElementAsDateTime("read_at"); + DateAdded = element.ElementAsDateTime("date_added"); + DateUpdated = element.ElementAsDateTime("date_updated"); + ReadCount = element.ElementAsInt("read_count"); + Body = element.ElementAsString("body"); + CommentsCount = element.ElementAsInt("comments_count"); + Url = element.ElementAsString("url"); + Owned = element.ElementAsInt("owned"); + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/SeriesResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/SeriesResource.cs new file mode 100644 index 000000000..1390e9434 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/SeriesResource.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// Represents information about a book series as defined by the Goodreads API. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class SeriesResource : GoodreadsResource + { + public SeriesResource() + { + Works = new List(); + } + + public override string ElementName => "series"; + + /// + /// The Id of the series. + /// + public long Id { get; private set; } + + /// + /// The title of the series. + /// + public string Title { get; private set; } + + /// + /// The description of the series. + /// + public string Description { get; private set; } + + /// + /// Any notes for the series. + /// + public string Note { get; private set; } + + /// + /// How many works are contained in the series total. + /// + public int SeriesWorksCount { get; private set; } + + /// + /// The count of works that are considered primary in the series. + /// + public int PrimaryWorksCount { get; private set; } + + /// + /// Determines if the series is usually numbered or not. + /// + public bool IsNumbered { get; private set; } + + /// + /// The list of works that are in this series. + /// Only populated if Goodreads returns it in the response. + /// + public List Works { get; set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + Title = element.ElementAsString("title", true); + Description = element.ElementAsString("description", true); + Note = element.ElementAsString("note", true); + SeriesWorksCount = element.ElementAsInt("series_works_count"); + PrimaryWorksCount = element.ElementAsInt("primary_work_count"); + IsNumbered = element.ElementAsBool("numbered"); + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/UserShelfResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/UserShelfResource.cs new file mode 100644 index 000000000..1bdc19960 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/UserShelfResource.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// Represents a user's shelf on their Goodreads profile. + /// + public sealed class UserShelfResource : GoodreadsResource + { + public override string ElementName => "shelf"; + + /// + /// The Id of this user shelf. + /// + public long Id { get; private set; } + + /// + /// The name of this user shelf. + /// + public string Name { get; private set; } + + /// + /// The number of books on this user shelf. + /// + public int BookCount { get; private set; } + + /// + /// Determines if this shelf is exclusive or not. + /// A single book can only be on one exclusive shelf. + /// + public bool IsExclusive { get; private set; } + + /// + /// The description of this user shelf. + /// + public string Description { get; private set; } + + /// + /// Determines the default sort column of this user shelf. + /// + public string Sort { get; private set; } + + /// + /// Determines the default sort order of this user shelf. + /// + // public Order? Order { get; private set; } + + /// + /// Determines if this shelf will be featured on the user's profile. + /// + public bool IsFeatured { get; private set; } + + /// + /// Determines if this user shelf is used in recommendations. + /// + public bool IsRecommendedFor { get; private set; } + + /// + /// Determines if this user shelf is sticky. + /// + public bool Sticky { get; private set; } + + /// + /// Determines if this user shelf is editable. + /// + public bool IsEditable { get; private set; } + + /// + /// The shelf created date. + /// + public DateTime? CreatedAt { get; private set; } + + /// + /// The shelf updated date. + /// + public DateTime? UpdatedAt { get; private set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + Name = element.ElementAsString("name"); + BookCount = element.ElementAsInt("book_count"); + Description = element.ElementAsString("description"); + Sort = element.ElementAsString("sort"); + IsExclusive = element.ElementAsBool("exclusive_flag"); + IsFeatured = element.ElementAsBool("featured"); + IsRecommendedFor = element.ElementAsBool("recommended_for"); + Sticky = element.ElementAsBool("sticky"); + IsEditable = element.ElementAsBool("editable_flag"); + CreatedAt = element.ElementAsDateTime("created_at"); + UpdatedAt = element.ElementAsDateTime("updated_at"); + + var orderElement = element.Element("order"); + if (orderElement != null) + { + var orderValue = orderElement.Value; + if (!string.IsNullOrWhiteSpace(orderValue)) + { + // if (orderValue == "a") + // { + // Order = Response.Order.Ascending; + // } + // else if (orderValue == "d") + // { + // Order = Response.Order.Descending; + // } + } + } + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/WorkResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/WorkResource.cs new file mode 100644 index 000000000..0260d6e62 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/GoodreadsResource/WorkResource.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models a work as defined by the Goodreads API. + /// A work is the root concept of something written. Each book + /// is a published edition of a piece of work. Most work properties + /// are aggregate information over all the editions of a work. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class WorkResource : GoodreadsResource + { + public override string ElementName => "work"; + + /// + /// The Goodreads Id for this work. + /// + public long Id { get; private set; } + + /// + /// The number of books for this work. + /// + public int BooksCount { get; private set; } + + /// + /// The Goodreads Book Id that is considered the best version of this work. + /// Might not be populated. See the property for details, if provided. + /// + public long? BestBookId { get; private set; } + + /// + /// The details for the best book of this work. Only populated + /// if Goodreads provides it as part of the response. + /// + public BestBookResource BestBook { get; private set; } + + public long SeriesLinkId { get; private set; } + + /// + /// If included in a list, this defines this work's position. + /// + public string UserPosition { get; private set; } + + /// + /// The number of reviews of this work. + /// + public int ReviewsCount { get; private set; } + + /// + /// The average rating of this work. + /// + public decimal AverageRating { get; private set; } + + /// + /// The number of ratings of this work. + /// + public int RatingsCount { get; private set; } + + /// + /// The number of text reviews of this work. + /// + public int TextReviewsCount { get; private set; } + + /// + /// The original publication date of this work. + /// + public DateTime? OriginalPublicationDate { get; private set; } + + /// + /// The original title of this work. + /// + public string OriginalTitle { get; private set; } + + /// + /// The original language of this work. + /// + public int? OriginalLanguageId { get; private set; } + + /// + /// The type of media for this work. + /// + public string MediaType { get; private set; } + + /// + /// The distribution of all the ratings for this work. + /// A dictionary of star rating -> number of ratings. + /// + public IReadOnlyDictionary RatingDistribution { get; private set; } + + public override void Parse(XElement element) + { + Id = element.ElementAsLong("id"); + + var bestBookElement = element.Element("best_book"); + if (bestBookElement != null) + { + BestBook = new BestBookResource(); + BestBook.Parse(bestBookElement); + } + + BestBookId = element.ElementAsNullableLong("best_book_id"); + BooksCount = element.ElementAsInt("books_count"); + ReviewsCount = element.ElementAsInt("reviews_count"); + + RatingsCount = element.ElementAsInt("ratings_count"); + + var average = element.ElementAsDecimal("average_rating"); + if (average == 0 && RatingsCount > 0) + { + average = element.ElementAsDecimal("ratings_sum") / RatingsCount; + } + + AverageRating = average; + + TextReviewsCount = element.ElementAsInt("text_reviews_count"); + + // Merge the Goodreads publication fields into one date property + var originalPublicationYear = element.ElementAsInt("original_publication_year"); + var originalPublicationMonth = element.ElementAsInt("original_publication_month"); + var originalPublicationDay = element.ElementAsInt("original_publication_day"); + if (originalPublicationYear != 0) + { + OriginalPublicationDate = new DateTime(originalPublicationYear, Math.Max(originalPublicationMonth, 1), Math.Max(originalPublicationDay, 1)); + } + + OriginalTitle = element.ElementAsString("original_title"); + OriginalLanguageId = element.ElementAsNullableInt("original_language_id"); + MediaType = element.ElementAsString("media_type"); + + // Parse out the rating distribution + var ratingDistributionElement = element.ElementAsString("rating_dist"); + if (ratingDistributionElement != null) + { + var parts = ratingDistributionElement.Split('|'); + if (parts.Length > 0) + { + var ratingDistribution = new Dictionary(); + + var ratings = parts.Select(x => x.Split(':')) + .Where(x => x[0] != "total") + .OrderBy(x => x[0]); + + foreach (var rating in ratings) + { + int star = 0, count = 0; + int.TryParse(rating[0], out star); + int.TryParse(rating[1], out count); + + ratingDistribution.Add(star, count); + } + + RatingDistribution = ratingDistribution; + } + } + } + + internal void SetSeriesInfo(XElement element) + { + SeriesLinkId = element.ElementAsLong("id"); + UserPosition = element.ElementAsString("user_position"); + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs deleted file mode 100644 index 16de7ee09..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumResource.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class AlbumResource - { - public string ArtistId { get; set; } - public List Artists { get; set; } - public string Disambiguation { get; set; } - public string Overview { get; set; } - public string Id { get; set; } - public List OldIds { get; set; } - public List Images { get; set; } - public List Links { get; set; } - public List Genres { get; set; } - public RatingResource Rating { get; set; } - public DateTime? ReleaseDate { get; set; } - public List Releases { get; set; } - public List SecondaryTypes { get; set; } - public string Title { get; set; } - public string Type { get; set; } - public List ReleaseStatuses { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs deleted file mode 100644 index ff56b4950..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class ArtistResource - { - public ArtistResource() - { - Albums = new List(); - Genres = new List(); - } - - public List Genres { get; set; } - public string AristUrl { get; set; } - public string Overview { get; set; } - public string Type { get; set; } - public string Disambiguation { get; set; } - public string Id { get; set; } - public List OldIds { get; set; } - public List Images { get; set; } - public List Links { get; set; } - public string ArtistName { get; set; } - public List ArtistAliases { get; set; } - public List Albums { get; set; } - public string Status { get; set; } - public RatingResource Rating { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EntityResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EntityResource.cs deleted file mode 100644 index ab2b5f987..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EntityResource.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class EntityResource - { - public int Score { get; set; } - public ArtistResource Artist { get; set; } - public AlbumResource Album { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs deleted file mode 100644 index 2e478c647..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class ImageResource - { - public string CoverType { get; set; } - public string Url { get; set; } - public int Height { get; set; } - public int Width { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/LinkResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/LinkResource.cs deleted file mode 100644 index 3021fbdfe..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/LinkResource.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class LinkResource - { - public string Target { get; set; } - public string Type { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MediumResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MediumResource.cs deleted file mode 100644 index 5f017010d..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MediumResource.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class MediumResource - { - public string Name { get; set; } - public string Format { get; set; } - public int Position { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RatingResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RatingResource.cs deleted file mode 100644 index c14e188e4..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RatingResource.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class RatingResource - { - public int Count { get; set; } - public decimal Value { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RecentUpdatesResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RecentUpdatesResource.cs deleted file mode 100644 index 530c9e858..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/RecentUpdatesResource.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class RecentUpdatesResource - { - public int Count { get; set; } - public bool Limited { get; set; } - public DateTime Since { get; set; } - public List Items { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ReleaseResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ReleaseResource.cs deleted file mode 100644 index d4f675feb..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ReleaseResource.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class ReleaseResource - { - public string Disambiguation { get; set; } - public List Country { get; set; } - public DateTime? ReleaseDate { get; set; } - public string Id { get; set; } - public List OldIds { get; set; } - public List Label { get; set; } - public List Media { get; set; } - public string Title { get; set; } - public string Status { get; set; } - public int TrackCount { get; set; } - public List Tracks { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackResource.cs deleted file mode 100644 index 611380de5..000000000 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackResource.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.MetadataSource.SkyHook.Resource -{ - public class TrackResource - { - public TrackResource() - { - } - - public string ArtistId { get; set; } - public int DurationMs { get; set; } - public string Id { get; set; } - public List OldIds { get; set; } - public string RecordingId { get; set; } - public List OldRecordingIds { get; set; } - public string TrackName { get; set; } - public string TrackNumber { get; set; } - public int TrackPosition { get; set; } - public bool Explicit { get; set; } - public int MediumNumber { get; set; } - } -} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 9d8e3fc7b..36c6ebb2c 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -6,87 +6,63 @@ using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.Music; -using NzbDrone.Core.Profiles.Metadata; namespace NzbDrone.Core.MetadataSource.SkyHook { - public class SkyHookProxy : IProvideArtistInfo, ISearchForNewArtist, IProvideAlbumInfo, ISearchForNewAlbum, ISearchForNewEntity + public class SkyHookProxy : IProvideAuthorInfo, ISearchForNewAuthor, IProvideBookInfo, ISearchForNewBook, ISearchForNewEntity { private readonly IHttpClient _httpClient; private readonly Logger _logger; - private readonly IArtistService _artistService; - private readonly IAlbumService _albumService; + private readonly IArtistService _authorService; + private readonly IAlbumService _bookService; private readonly IMetadataRequestBuilder _requestBuilder; - private readonly IMetadataProfileService _metadataProfileService; private readonly ICached> _cache; - private static readonly List NonAudioMedia = new List { "DVD", "DVD-Video", "Blu-ray", "HD-DVD", "VCD", "SVCD", "UMD", "VHS" }; - private static readonly List SkippedTracks = new List { "[data track]" }; - public SkyHookProxy(IHttpClient httpClient, IMetadataRequestBuilder requestBuilder, - IArtistService artistService, + IArtistService authorService, IAlbumService albumService, Logger logger, - IMetadataProfileService metadataProfileService, ICacheManager cacheManager) { _httpClient = httpClient; - _metadataProfileService = metadataProfileService; _requestBuilder = requestBuilder; - _artistService = artistService; - _albumService = albumService; + _authorService = authorService; + _bookService = albumService; _cache = cacheManager.GetCache>(GetType()); _logger = logger; } public HashSet GetChangedArtists(DateTime startTime) { - var startTimeUtc = (DateTimeOffset)DateTime.SpecifyKind(startTime, DateTimeKind.Utc); - var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", "recent/artist") - .AddQueryParam("since", startTimeUtc.ToUnixTimeSeconds()) - .Build(); - - httpRequest.SuppressHttpError = true; - - var httpResponse = _httpClient.Get(httpRequest); - - if (httpResponse.Resource.Limited) - { - return null; - } - - return new HashSet(httpResponse.Resource.Items); + return null; } - public Artist GetArtistInfo(string foreignArtistId, int metadataProfileId) + public Author GetAuthorInfo(string foreignAuthorId) { - _logger.Debug("Getting Artist with ReadarrAPI.MetadataID of {0}", foreignArtistId); + _logger.Debug("Getting Author details ReadarrAPI.MetadataID of {0}", foreignAuthorId); var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", "artist/" + foreignArtistId) - .Build(); + .SetSegment("route", $"author/{foreignAuthorId}") + .Build(); httpRequest.AllowAutoRedirect = true; httpRequest.SuppressHttpError = true; - var httpResponse = _httpClient.Get(httpRequest); + var httpResponse = _httpClient.Get(httpRequest); if (httpResponse.HasHttpError) { if (httpResponse.StatusCode == HttpStatusCode.NotFound) { - throw new ArtistNotFoundException(foreignArtistId); + throw new ArtistNotFoundException(foreignAuthorId); } else if (httpResponse.StatusCode == HttpStatusCode.BadRequest) { - throw new BadRequestException(foreignArtistId); + throw new BadRequestException(foreignAuthorId); } else { @@ -94,15 +70,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - var artist = new Artist(); - artist.Metadata = MapArtistMetadata(httpResponse.Resource); - artist.CleanName = Parser.Parser.CleanArtistName(artist.Metadata.Value.Name); - artist.SortName = Parser.Parser.NormalizeTitle(artist.Metadata.Value.Name); - - artist.Albums = FilterAlbums(httpResponse.Resource.Albums, metadataProfileId) - .Select(x => MapAlbum(x, null)).ToList(); - - return artist; + return MapAuthor(httpResponse.Resource); } public HashSet GetChangedAlbums(DateTime startTime) @@ -112,59 +80,31 @@ namespace NzbDrone.Core.MetadataSource.SkyHook private HashSet GetChangedAlbumsUncached(DateTime startTime) { - var startTimeUtc = (DateTimeOffset)DateTime.SpecifyKind(startTime, DateTimeKind.Utc); - var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", "recent/album") - .AddQueryParam("since", startTimeUtc.ToUnixTimeSeconds()) - .Build(); - - httpRequest.SuppressHttpError = true; - - var httpResponse = _httpClient.Get(httpRequest); - - if (httpResponse.Resource.Limited) - { - return null; - } - - return new HashSet(httpResponse.Resource.Items); - } - - public IEnumerable FilterAlbums(IEnumerable albums, int metadataProfileId) - { - var metadataProfile = _metadataProfileService.Exists(metadataProfileId) ? _metadataProfileService.Get(metadataProfileId) : _metadataProfileService.All().First(); - var primaryTypes = new HashSet(metadataProfile.PrimaryAlbumTypes.Where(s => s.Allowed).Select(s => s.PrimaryAlbumType.Name)); - var secondaryTypes = new HashSet(metadataProfile.SecondaryAlbumTypes.Where(s => s.Allowed).Select(s => s.SecondaryAlbumType.Name)); - var releaseStatuses = new HashSet(metadataProfile.ReleaseStatuses.Where(s => s.Allowed).Select(s => s.ReleaseStatus.Name)); - - return albums.Where(album => primaryTypes.Contains(album.Type) && - ((!album.SecondaryTypes.Any() && secondaryTypes.Contains("Studio")) || - album.SecondaryTypes.Any(x => secondaryTypes.Contains(x))) && - album.ReleaseStatuses.Any(x => releaseStatuses.Contains(x))); + return null; } - public Tuple> GetAlbumInfo(string foreignAlbumId) + public Tuple> GetBookInfo(string foreignBookId) { - _logger.Debug("Getting Album with ReadarrAPI.MetadataID of {0}", foreignAlbumId); + _logger.Debug("Getting Book with ReadarrAPI.MetadataID of {0}", foreignBookId); var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", "album/" + foreignAlbumId) - .Build(); + .SetSegment("route", $"book/{foreignBookId}") + .Build(); httpRequest.AllowAutoRedirect = true; httpRequest.SuppressHttpError = true; - var httpResponse = _httpClient.Get(httpRequest); + var httpResponse = _httpClient.Get(httpRequest); if (httpResponse.HasHttpError) { if (httpResponse.StatusCode == HttpStatusCode.NotFound) { - throw new AlbumNotFoundException(foreignAlbumId); + throw new AlbumNotFoundException(foreignBookId); } else if (httpResponse.StatusCode == HttpStatusCode.BadRequest) { - throw new BadRequestException(foreignAlbumId); + throw new BadRequestException(foreignBookId); } else { @@ -172,127 +112,75 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - var artists = httpResponse.Resource.Artists.Select(MapArtistMetadata).ToList(); - var artistDict = artists.ToDictionary(x => x.ForeignArtistId, x => x); - var album = MapAlbum(httpResponse.Resource, artistDict); - album.ArtistMetadata = artistDict[httpResponse.Resource.ArtistId]; + var b = httpResponse.Resource; + var book = MapBook(b); - return new Tuple>(httpResponse.Resource.ArtistId, album, artists); + var authors = httpResponse.Resource.AuthorMetadata.SelectList(MapAuthor); + var authorid = GetAuthorId(b); + book.AuthorMetadata = authors.First(x => x.ForeignAuthorId == authorid); + + return new Tuple>(authorid, book, authors); } - public List SearchForNewArtist(string title) + public List SearchForNewAuthor(string title) { - try - { - var lowerTitle = title.ToLowerInvariant(); - - if (lowerTitle.StartsWith("readarr:") || lowerTitle.StartsWith("readarrid:") || lowerTitle.StartsWith("mbid:")) - { - var slug = lowerTitle.Split(':')[1].Trim(); - - Guid searchGuid; + var books = SearchForNewBook(title, null); - bool isValid = Guid.TryParse(slug, out searchGuid); - - if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || isValid == false) - { - return new List(); - } - - try - { - var existingArtist = _artistService.FindById(searchGuid.ToString()); - if (existingArtist != null) - { - return new List { existingArtist }; - } - - var metadataProfile = _metadataProfileService.All().First().Id; //Change this to Use last Used profile? - - return new List { GetArtistInfo(searchGuid.ToString(), metadataProfile) }; - } - catch (ArtistNotFoundException) - { - return new List(); - } - } - - var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", "search") - .AddQueryParam("type", "artist") - .AddQueryParam("query", title.ToLower().Trim()) - .Build(); - - var httpResponse = _httpClient.Get>(httpRequest); - - return httpResponse.Resource.SelectList(MapSearchResult); - } - catch (HttpException) - { - throw new SkyHookException("Search for '{0}' failed. Unable to communicate with ReadarrAPI.", title); - } - catch (Exception ex) - { - _logger.Warn(ex, ex.Message); - throw new SkyHookException("Search for '{0}' failed. Invalid response received from ReadarrAPI.", title); - } + return books.Select(x => x.Author.Value).ToList(); } - public List SearchForNewAlbum(string title, string artist) + public List SearchForNewBook(string title, string artist) { try { var lowerTitle = title.ToLowerInvariant(); - if (lowerTitle.StartsWith("readarr:") || lowerTitle.StartsWith("readarrid:") || lowerTitle.StartsWith("mbid:")) - { - var slug = lowerTitle.Split(':')[1].Trim(); + var split = lowerTitle.Split(':'); + var prefix = split[0]; - Guid searchGuid; - - bool isValid = Guid.TryParse(slug, out searchGuid); + if (split.Length == 2 && new[] { "readarr", "readarrid", "goodreads", "isbn", "asin" }.Contains(prefix)) + { + var slug = split[1].Trim(); - if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || isValid == false) + if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace)) { - return new List(); + return new List(); } - try + if (prefix == "goodreads" || prefix == "readarr" || prefix == "readarrid") { - var existingAlbum = _albumService.FindById(searchGuid.ToString()); - - if (existingAlbum == null) + var isValid = int.TryParse(slug, out var searchId); + if (!isValid) { - var data = GetAlbumInfo(searchGuid.ToString()); - var album = data.Item2; - album.Artist = _artistService.FindById(data.Item1) ?? new Artist - { - Metadata = data.Item3.Single(x => x.ForeignArtistId == data.Item1) - }; - - return new List { album }; + return new List(); } - existingAlbum.Artist = _artistService.GetArtist(existingAlbum.ArtistId); - return new List { existingAlbum }; + return SearchByGoodreadsId(searchId); + } + else if (prefix == "isbn") + { + return SearchByIsbn(slug); } - catch (ArtistNotFoundException) + else if (prefix == "asin") { - return new List(); + return SearchByAsin(slug); } } + var q = title.ToLower().Trim(); + if (artist != null) + { + q += " " + artist; + } + var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", "search") - .AddQueryParam("type", "album") - .AddQueryParam("query", title.ToLower().Trim()) - .AddQueryParam("artist", artist.IsNotNullOrWhiteSpace() ? artist.ToLower().Trim() : string.Empty) - .AddQueryParam("includeTracks", "1") - .Build(); + .SetSegment("route", "search") + .AddQueryParam("q", q) + .Build(); - var httpResponse = _httpClient.Get>(httpRequest); + var result = _httpClient.Get(httpRequest); - return httpResponse.Resource.SelectList(MapSearchResult); + return MapSearchResult(result.Resource); } catch (HttpException) { @@ -305,315 +193,263 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - public List SearchForNewAlbumByRecordingIds(List recordingIds) + public List SearchByIsbn(string isbn) { - var ids = recordingIds.Where(x => x.IsNotNullOrWhiteSpace()).Distinct(); - var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", "search/fingerprint") - .Build(); - - httpRequest.SetContent(ids.ToJson()); - httpRequest.Headers.ContentType = "application/json"; + return SearchByAlternateId("isbn", isbn); + } - var httpResponse = _httpClient.Post>(httpRequest); + public List SearchByAsin(string asin) + { + return SearchByAlternateId("asin", asin.ToUpper()); + } - return httpResponse.Resource.SelectList(MapSearchResult); + public List SearchByGoodreadsId(int goodreadsId) + { + return SearchByAlternateId("goodreads", goodreadsId.ToString()); } - public List SearchForNewEntity(string title) + private List SearchByAlternateId(string type, string id) { try { var httpRequest = _requestBuilder.GetRequestBuilder().Create() - .SetSegment("route", "search") - .AddQueryParam("type", "all") - .AddQueryParam("query", title.ToLower().Trim()) - .Build(); + .SetSegment("route", $"book/{type}/{id}") + .Build(); - var httpResponse = _httpClient.Get>(httpRequest); + var httpResponse = _httpClient.Get(httpRequest); - return httpResponse.Resource.SelectList(MapSearchResult); + var result = _httpClient.Get(httpRequest); + + return MapSearchResult(result.Resource); } catch (HttpException) { - throw new SkyHookException("Search for '{0}' failed. Unable to communicate with ReadarrAPI.", title); + throw new SkyHookException("Search for {0} '{1}' failed. Unable to communicate with ReadarrAPI.", type, id); } catch (Exception ex) { _logger.Warn(ex, ex.Message); - throw new SkyHookException("Search for '{0}' failed. Invalid response received from ReadarrAPI.", title); + throw new SkyHookException("Search for {0 }'{1}' failed. Invalid response received from ReadarrAPI.", type, id); } } - private Artist MapSearchResult(ArtistResource resource) + public List SearchForNewAlbumByRecordingIds(List recordingIds) { - var artist = _artistService.FindById(resource.Id); - if (artist == null) - { - artist = new Artist(); - artist.Metadata = MapArtistMetadata(resource); - } - - return artist; + return null; } - private Album MapSearchResult(AlbumResource resource) + public List SearchForNewEntity(string title) { - var artists = resource.Artists.Select(MapArtistMetadata).ToDictionary(x => x.ForeignArtistId, x => x); + var books = SearchForNewBook(title, null); - var artist = _artistService.FindById(resource.ArtistId); - if (artist == null) + var result = new List(); + foreach (var book in books) { - artist = new Artist(); - artist.Metadata = artists[resource.ArtistId]; - } + var author = book.Author.Value; + + if (!result.Contains(author)) + { + result.Add(author); + } - var album = _albumService.FindById(resource.Id) ?? MapAlbum(resource, artists); - album.Artist = artist; - album.ArtistMetadata = artist.Metadata.Value; + result.Add(book); + } - return album; + return result; } - private object MapSearchResult(EntityResource resource) + private Author MapAuthor(AuthorResource resource) { - if (resource.Artist != null) - { - return MapSearchResult(resource.Artist); - } - else + var metadata = MapAuthor(resource.AuthorMetadata.First(x => x.ForeignId == resource.ForeignId)); + + var books = resource.Books + .Where(x => GetAuthorId(x) == resource.ForeignId) + .Select(MapBook) + .ToList(); + + books.ForEach(x => x.AuthorMetadata = metadata); + + var series = resource.Series.Select(MapSeries).ToList(); + + MapSeriesLinks(series, books, resource); + + var result = new Author { - return MapSearchResult(resource.Album); - } + Metadata = metadata, + CleanName = Parser.Parser.CleanArtistName(metadata.Name), + SortName = Parser.Parser.NormalizeTitle(metadata.Name), + Books = books, + Series = series + }; + + return result; } - private static Album MapAlbum(AlbumResource resource, Dictionary artistDict) + private void MapSeriesLinks(List series, List books, BulkResource resource) { - Album album = new Album(); - album.ForeignAlbumId = resource.Id; - album.OldForeignAlbumIds = resource.OldIds; - album.Title = resource.Title; - album.Overview = resource.Overview; - album.Disambiguation = resource.Disambiguation; - album.ReleaseDate = resource.ReleaseDate; - - if (resource.Images != null) - { - album.Images = resource.Images.Select(MapImage).ToList(); - } - - album.AlbumType = resource.Type; - album.SecondaryTypes = resource.SecondaryTypes.Select(MapSecondaryTypes).ToList(); - album.Ratings = MapRatings(resource.Rating); - album.Links = resource.Links?.Select(MapLink).ToList(); - album.Genres = resource.Genres; - album.CleanTitle = Parser.Parser.CleanArtistName(album.Title); + var bookDict = books.ToDictionary(x => x.ForeignBookId); + var seriesDict = series.ToDictionary(x => x.ForeignSeriesId); - if (resource.Releases != null) + // only take series where there are some works + foreach (var s in resource.Series.Where(x => x.BookLinks.Any())) { - album.AlbumReleases = resource.Releases.Select(x => MapRelease(x, artistDict)).Where(x => x.TrackCount > 0).ToList(); - - // Monitor the release with most tracks - var mostTracks = album.AlbumReleases.Value.OrderByDescending(x => x.TrackCount).FirstOrDefault(); - if (mostTracks != null) + if (seriesDict.TryGetValue(s.ForeignId, out var curr)) { - mostTracks.Monitored = true; + curr.LinkItems = s.BookLinks.Where(x => bookDict.ContainsKey(x.BookId)).Select(l => new SeriesBookLink + { + Book = bookDict[l.BookId], + Series = curr, + IsPrimary = l.Primary + }).ToList(); } } - else + + foreach (var b in resource.Books) { - album.AlbumReleases = new List(); + if (bookDict.TryGetValue(b.ForeignId, out var curr)) + { + curr.SeriesLinks = b.SeriesLinks.Where(l => seriesDict.ContainsKey(l.SeriesId)).Select(l => new SeriesBookLink + { + Series = seriesDict[l.SeriesId], + Position = l.Position, + Book = curr + }).ToList(); + } } - album.AnyReleaseOk = true; - - return album; + _ = series.SelectMany(x => x.LinkItems.Value) + .Join(books.SelectMany(x => x.SeriesLinks.Value), + sl => Tuple.Create(sl.Series.Value.ForeignSeriesId, sl.Book.Value.ForeignBookId), + bl => Tuple.Create(bl.Series.Value.ForeignSeriesId, bl.Book.Value.ForeignBookId), + (sl, bl) => + { + sl.Position = bl.Position; + bl.IsPrimary = sl.IsPrimary; + return sl; + }).ToList(); } - private static AlbumRelease MapRelease(ReleaseResource resource, Dictionary artistDict) + private static AuthorMetadata MapAuthor(AuthorSummaryResource resource) { - AlbumRelease release = new AlbumRelease(); - release.ForeignReleaseId = resource.Id; - release.OldForeignReleaseIds = resource.OldIds; - release.Title = resource.Title; - release.Status = resource.Status; - release.Label = resource.Label; - release.Disambiguation = resource.Disambiguation; - release.Country = resource.Country; - release.ReleaseDate = resource.ReleaseDate; - - // Get the complete set of media/tracks returned by the API, adding missing media if necessary - var allMedia = resource.Media.Select(MapMedium).ToList(); - var allTracks = resource.Tracks.Select(x => MapTrack(x, artistDict)); - if (!allMedia.Any()) + var author = new AuthorMetadata + { + ForeignAuthorId = resource.ForeignId, + GoodreadsId = resource.GoodreadsId, + TitleSlug = resource.TitleSlug, + Name = resource.Name.CleanSpaces(), + Overview = resource.Description, + Ratings = new Ratings { Votes = resource.RatingsCount, Value = (decimal)resource.AverageRating } + }; + + if (resource.ImageUrl.IsNotNullOrWhiteSpace()) { - foreach (int n in allTracks.Select(x => x.MediumNumber).Distinct()) + author.Images.Add(new MediaCover.MediaCover { - allMedia.Add(new Medium { Name = "Unknown", Number = n, Format = "Unknown" }); - } + Url = resource.ImageUrl, + CoverType = MediaCoverTypes.Poster + }); } - // Skip non-audio media - var audioMediaNumbers = allMedia.Where(x => !NonAudioMedia.Contains(x.Format)).Select(x => x.Number); - - // Get tracks on the audio media and omit any that are skipped - release.Tracks = allTracks.Where(x => audioMediaNumbers.Contains(x.MediumNumber) && !SkippedTracks.Contains(x.Title)).ToList(); - release.TrackCount = release.Tracks.Value.Count; - - // Only include the media that contain the tracks we have selected - var usedMediaNumbers = release.Tracks.Value.Select(track => track.MediumNumber); - release.Media = allMedia.Where(medium => usedMediaNumbers.Contains(medium.Number)).ToList(); + author.Links.Add(new Links { Url = resource.WebUrl, Name = "Goodreads" }); - release.Duration = release.Tracks.Value.Sum(x => x.Duration); - - return release; + return author; } - private static Medium MapMedium(MediumResource resource) + private static Series MapSeries(SeriesResource resource) { - Medium medium = new Medium + var series = new Series { - Name = resource.Name, - Number = resource.Position, - Format = resource.Format + ForeignSeriesId = resource.ForeignId, + Title = resource.Title, + Description = resource.Description }; - return medium; + return series; } - private static Track MapTrack(TrackResource resource, Dictionary artistDict) + private static Book MapBook(BookResource resource) { - Track track = new Track + var book = new Book { - ArtistMetadata = artistDict[resource.ArtistId], - Title = resource.TrackName, - ForeignTrackId = resource.Id, - OldForeignTrackIds = resource.OldIds, - ForeignRecordingId = resource.RecordingId, - OldForeignRecordingIds = resource.OldRecordingIds, - TrackNumber = resource.TrackNumber, - AbsoluteTrackNumber = resource.TrackPosition, - Duration = resource.DurationMs, - MediumNumber = resource.MediumNumber + ForeignBookId = resource.ForeignId, + ForeignWorkId = resource.WorkForeignId, + GoodreadsId = resource.GoodreadsId, + TitleSlug = resource.TitleSlug, + Isbn13 = resource.Isbn13, + Asin = resource.Asin, + Title = resource.Title.CleanSpaces(), + Language = resource.Language, + Publisher = resource.Publisher, + CleanTitle = Parser.Parser.CleanArtistName(resource.Title), + Overview = resource.Description, + ReleaseDate = resource.ReleaseDate, + Ratings = new Ratings { Votes = resource.RatingCount, Value = (decimal)resource.AverageRating } }; - return track; - } - - private static ArtistMetadata MapArtistMetadata(ArtistResource resource) - { - ArtistMetadata artist = new ArtistMetadata(); - - artist.Name = resource.ArtistName; - artist.Aliases = resource.ArtistAliases; - artist.ForeignArtistId = resource.Id; - artist.OldForeignArtistIds = resource.OldIds; - artist.Genres = resource.Genres; - artist.Overview = resource.Overview; - artist.Disambiguation = resource.Disambiguation; - artist.Type = resource.Type; - artist.Status = MapArtistStatus(resource.Status); - artist.Ratings = MapRatings(resource.Rating); - artist.Images = resource.Images?.Select(MapImage).ToList(); - artist.Links = resource.Links?.Select(MapLink).ToList(); - return artist; - } - - private static ArtistStatusType MapArtistStatus(string status) - { - if (status == null) + if (resource.ImageUrl.IsNotNullOrWhiteSpace()) { - return ArtistStatusType.Continuing; + book.Images.Add(new MediaCover.MediaCover + { + Url = resource.ImageUrl, + CoverType = MediaCoverTypes.Cover + }); } - if (status.Equals("ended", StringComparison.InvariantCultureIgnoreCase)) - { - return ArtistStatusType.Ended; - } + book.Links.Add(new Links { Url = resource.WebUrl, Name = "Goodreads" }); - return ArtistStatusType.Continuing; + return book; } - private static Ratings MapRatings(RatingResource rating) + private List MapSearchResult(BookSearchResource resource) { - if (rating == null) - { - return new Ratings(); - } + var metadata = resource.AuthorMetadata.SelectList(MapAuthor).ToDictionary(x => x.ForeignAuthorId); - return new Ratings - { - Votes = rating.Count, - Value = rating.Value - }; - } + var result = new List(); - private static MediaCover.MediaCover MapImage(ImageResource arg) - { - return new MediaCover.MediaCover + foreach (var b in resource.Books) { - Url = arg.Url, - CoverType = MapCoverType(arg.CoverType) - }; - } + var book = _bookService.FindById(b.ForeignId); + if (book == null) + { + book = MapBook(b); - private static Links MapLink(LinkResource arg) - { - return new Links - { - Url = arg.Target, - Name = arg.Type - }; - } + var authorid = GetAuthorId(b); - private static MediaCoverTypes MapCoverType(string coverType) - { - switch (coverType.ToLower()) - { - case "poster": - return MediaCoverTypes.Poster; - case "banner": - return MediaCoverTypes.Banner; - case "fanart": - return MediaCoverTypes.Fanart; - case "cover": - return MediaCoverTypes.Cover; - case "disc": - return MediaCoverTypes.Disc; - case "logo": - return MediaCoverTypes.Logo; - default: - return MediaCoverTypes.Unknown; + if (authorid == null) + { + continue; + } + + var author = _authorService.FindById(authorid); + + if (author == null) + { + var authorMetadata = metadata[authorid]; + + author = new Author + { + CleanName = Parser.Parser.CleanArtistName(authorMetadata.Name), + Metadata = authorMetadata + }; + } + + book.Author = author; + book.AuthorMetadata = author.Metadata.Value; + } + + result.Add(book); } + + var seriesList = resource.Series.Select(MapSeries).ToList(); + + MapSeriesLinks(seriesList, result, resource); + + return result; } - public static SecondaryAlbumType MapSecondaryTypes(string albumType) + private string GetAuthorId(BookResource b) { - switch (albumType.ToLowerInvariant()) - { - case "compilation": - return SecondaryAlbumType.Compilation; - case "soundtrack": - return SecondaryAlbumType.Soundtrack; - case "spokenword": - return SecondaryAlbumType.Spokenword; - case "interview": - return SecondaryAlbumType.Interview; - case "audiobook": - return SecondaryAlbumType.Audiobook; - case "live": - return SecondaryAlbumType.Live; - case "remix": - return SecondaryAlbumType.Remix; - case "dj-mix": - return SecondaryAlbumType.DJMix; - case "mixtape/street": - return SecondaryAlbumType.Mixtape; - case "demo": - return SecondaryAlbumType.Demo; - default: - return SecondaryAlbumType.Studio; - } + return b.Contributors.FirstOrDefault()?.ForeignId; } } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/AuthorResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/AuthorResource.cs new file mode 100644 index 000000000..bdb3602bf --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/AuthorResource.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook +{ + public class AuthorResource : BulkResource + { + public string ForeignId { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/AuthorSummaryResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/AuthorSummaryResource.cs new file mode 100644 index 000000000..9fc3f4fe0 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/AuthorSummaryResource.cs @@ -0,0 +1,19 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook +{ + public class AuthorSummaryResource + { + public string ForeignId { get; set; } + public int GoodreadsId { get; set; } + public string TitleSlug { get; set; } + public string Name { get; set; } + + public string Description { get; set; } + public string ImageUrl { get; set; } + public string ProfileUri { get; set; } + public string WebUrl { get; set; } + + public int ReviewCount { get; set; } + public int RatingsCount { get; set; } + public double AverageRating { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BookResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BookResource.cs new file mode 100644 index 000000000..2b187bc7b --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BookResource.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.MetadataSource.SkyHook +{ + public class BookResource + { + public string ForeignId { get; set; } + public int GoodreadsId { get; set; } + public string TitleSlug { get; set; } + public string Asin { get; set; } + public string Description { get; set; } + public string Isbn13 { get; set; } + public long Rvn { get; set; } + public string Title { get; set; } + public string Publisher { get; set; } + public string Language { get; set; } + public string DisplayGroup { get; set; } + public string ImageUrl { get; set; } + public string KindleMappingStatus { get; set; } + public string Marketplace { get; set; } + public int? NumPages { get; set; } + public int ReviewsCount { get; set; } + public int RatingCount { get; set; } + public double AverageRating { get; set; } + public IList SeriesLinks { get; set; } = new List(); + public string WebUrl { get; set; } + public string WorkForeignId { get; set; } + public DateTime? ReleaseDate { get; set; } + + public List Contributors { get; set; } = new List(); + public List AuthorMetadata { get; set; } = new List(); + } + + public class BookSeriesLinkResource + { + public string SeriesId { get; set; } + public string Position { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BookSearchResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BookSearchResource.cs new file mode 100644 index 000000000..bc8689c14 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BookSearchResource.cs @@ -0,0 +1,6 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook +{ + public class BookSearchResource : BulkResource + { + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BulkResourceBase.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BulkResourceBase.cs new file mode 100644 index 000000000..bae551d88 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/BulkResourceBase.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MetadataSource.SkyHook +{ + public class BulkResource + { + public List AuthorMetadata { get; set; } = new List(); + public List Books { get; set; } + public List Series { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/ContributorResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/ContributorResource.cs new file mode 100644 index 000000000..c6f632b09 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/ContributorResource.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.MetadataSource.SkyHook +{ + public class ContributorResource + { + public string ForeignId { get; set; } + public string Role { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/SeriesResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/SeriesResource.cs new file mode 100644 index 000000000..0062de038 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookResource/SeriesResource.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MetadataSource.SkyHook +{ + public class SeriesResource + { + public string ForeignId { get; set; } + public string Title { get; set; } + public string Description { get; set; } + + public List BookLinks { get; set; } + } + + public class SeriesBookLinkResource + { + public string BookId { get; set; } + public bool Primary { get; set; } + } +} diff --git a/src/NzbDrone.Core/Music/Events/AlbumAddedEvent.cs b/src/NzbDrone.Core/Music/Events/AlbumAddedEvent.cs deleted file mode 100644 index bbd98811e..000000000 --- a/src/NzbDrone.Core/Music/Events/AlbumAddedEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Music.Events -{ - public class AlbumAddedEvent : IEvent - { - public Album Album { get; private set; } - - public AlbumAddedEvent(Album album) - { - Album = album; - } - } -} diff --git a/src/NzbDrone.Core/Music/Events/AlbumInfoRefreshedEvent.cs b/src/NzbDrone.Core/Music/Events/AlbumInfoRefreshedEvent.cs deleted file mode 100644 index 25ad9aca3..000000000 --- a/src/NzbDrone.Core/Music/Events/AlbumInfoRefreshedEvent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Music.Events -{ - public class AlbumInfoRefreshedEvent : IEvent - { - public Artist Artist { get; set; } - public ReadOnlyCollection Added { get; private set; } - public ReadOnlyCollection Updated { get; private set; } - - public AlbumInfoRefreshedEvent(Artist artist, IList added, IList updated) - { - Artist = artist; - Added = new ReadOnlyCollection(added); - Updated = new ReadOnlyCollection(updated); - } - } -} diff --git a/src/NzbDrone.Core/Music/Events/ArtistsImportedEvent.cs b/src/NzbDrone.Core/Music/Events/ArtistsImportedEvent.cs deleted file mode 100644 index 0b3833b15..000000000 --- a/src/NzbDrone.Core/Music/Events/ArtistsImportedEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Music.Events -{ - public class ArtistsImportedEvent : IEvent - { - public List ArtistIds { get; private set; } - - public ArtistsImportedEvent(List artistIds) - { - ArtistIds = artistIds; - } - } -} diff --git a/src/NzbDrone.Core/Music/Events/ReleaseDeletedEvent.cs b/src/NzbDrone.Core/Music/Events/ReleaseDeletedEvent.cs deleted file mode 100644 index cd4fa2254..000000000 --- a/src/NzbDrone.Core/Music/Events/ReleaseDeletedEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Music.Events -{ - public class ReleaseDeletedEvent : IEvent - { - public AlbumRelease Release { get; private set; } - - public ReleaseDeletedEvent(AlbumRelease release) - { - Release = release; - } - } -} diff --git a/src/NzbDrone.Core/Music/Model/Medium.cs b/src/NzbDrone.Core/Music/Model/Medium.cs deleted file mode 100644 index 63a9a72c4..000000000 --- a/src/NzbDrone.Core/Music/Model/Medium.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Equ; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Music -{ - public class Medium : MemberwiseEquatable, IEmbeddedDocument - { - public int Number { get; set; } - public string Name { get; set; } - public string Format { get; set; } - } -} diff --git a/src/NzbDrone.Core/Music/Model/Member.cs b/src/NzbDrone.Core/Music/Model/Member.cs deleted file mode 100644 index 34f3bcbc2..000000000 --- a/src/NzbDrone.Core/Music/Model/Member.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using Equ; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Music -{ - public class Member : MemberwiseEquatable, IEmbeddedDocument - { - public Member() - { - Images = new List(); - } - - public string Name { get; set; } - public string Instrument { get; set; } - public List Images { get; set; } - } -} diff --git a/src/NzbDrone.Core/Music/Model/PrimaryAlbumType.cs b/src/NzbDrone.Core/Music/Model/PrimaryAlbumType.cs deleted file mode 100644 index 068bf1779..000000000 --- a/src/NzbDrone.Core/Music/Model/PrimaryAlbumType.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Music -{ - public class PrimaryAlbumType : IEmbeddedDocument, IEquatable - { - public int Id { get; set; } - public string Name { get; set; } - - public PrimaryAlbumType() - { - } - - private PrimaryAlbumType(int id, string name) - { - Id = id; - Name = name; - } - - public override string ToString() - { - return Name; - } - - public override int GetHashCode() - { - return Id.GetHashCode(); - } - - public bool Equals(PrimaryAlbumType other) - { - if (ReferenceEquals(null, other)) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return Id.Equals(other.Id); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return ReferenceEquals(this, obj) || Equals(obj as PrimaryAlbumType); - } - - public static bool operator ==(PrimaryAlbumType left, PrimaryAlbumType right) - { - return Equals(left, right); - } - - public static bool operator !=(PrimaryAlbumType left, PrimaryAlbumType right) - { - return !Equals(left, right); - } - - public static PrimaryAlbumType Album => new PrimaryAlbumType(0, "Album"); - public static PrimaryAlbumType EP => new PrimaryAlbumType(1, "EP"); - public static PrimaryAlbumType Single => new PrimaryAlbumType(2, "Single"); - public static PrimaryAlbumType Broadcast => new PrimaryAlbumType(3, "Broadcast"); - public static PrimaryAlbumType Other => new PrimaryAlbumType(4, "Other"); - - public static readonly List All = new List - { - Album, - EP, - Single, - Broadcast, - Other - }; - - public static PrimaryAlbumType FindById(int id) - { - if (id == 0) - { - return Album; - } - - PrimaryAlbumType albumType = All.FirstOrDefault(v => v.Id == id); - - if (albumType == null) - { - throw new ArgumentException(@"ID does not match a known album type", nameof(id)); - } - - return albumType; - } - - public static explicit operator PrimaryAlbumType(int id) - { - return FindById(id); - } - - public static explicit operator int(PrimaryAlbumType albumType) - { - return albumType.Id; - } - - public static explicit operator PrimaryAlbumType(string type) - { - var albumType = All.FirstOrDefault(v => v.Name.Equals(type, StringComparison.InvariantCultureIgnoreCase)); - - if (albumType == null) - { - throw new ArgumentException(@"Type does not match a known album type", nameof(type)); - } - - return albumType; - } - } -} diff --git a/src/NzbDrone.Core/Music/Model/Release.cs b/src/NzbDrone.Core/Music/Model/Release.cs deleted file mode 100644 index 1eea6f810..000000000 --- a/src/NzbDrone.Core/Music/Model/Release.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using Equ; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Music -{ - public class AlbumRelease : Entity - { - public AlbumRelease() - { - OldForeignReleaseIds = new List(); - Label = new List(); - Country = new List(); - Media = new List(); - } - - // These correspond to columns in the AlbumReleases table - public int AlbumId { get; set; } - public string ForeignReleaseId { get; set; } - public List OldForeignReleaseIds { get; set; } - public string Title { get; set; } - public string Status { get; set; } - public int Duration { get; set; } - public List Label { get; set; } - public string Disambiguation { get; set; } - public List Country { get; set; } - public DateTime? ReleaseDate { get; set; } - public List Media { get; set; } - public int TrackCount { get; set; } - public bool Monitored { get; set; } - - // These are dynamically queried from other tables - [MemberwiseEqualityIgnore] - public LazyLoaded Album { get; set; } - [MemberwiseEqualityIgnore] - public LazyLoaded> Tracks { get; set; } - - public override string ToString() - { - return string.Format("[{0}][{1}]", ForeignReleaseId, Title.NullSafe()); - } - - public override void UseMetadataFrom(AlbumRelease other) - { - ForeignReleaseId = other.ForeignReleaseId; - OldForeignReleaseIds = other.OldForeignReleaseIds; - Title = other.Title; - Status = other.Status; - Duration = other.Duration; - Label = other.Label; - Disambiguation = other.Disambiguation; - Country = other.Country; - ReleaseDate = other.ReleaseDate; - Media = other.Media; - TrackCount = other.TrackCount; - } - - public override void UseDbFieldsFrom(AlbumRelease other) - { - Id = other.Id; - AlbumId = other.AlbumId; - Album = other.Album; - Monitored = other.Monitored; - } - } -} diff --git a/src/NzbDrone.Core/Music/Model/ReleaseStatus.cs b/src/NzbDrone.Core/Music/Model/ReleaseStatus.cs deleted file mode 100644 index b22f08ba2..000000000 --- a/src/NzbDrone.Core/Music/Model/ReleaseStatus.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Music -{ - public class ReleaseStatus : IEmbeddedDocument, IEquatable - { - public int Id { get; set; } - public string Name { get; set; } - - public ReleaseStatus() - { - } - - private ReleaseStatus(int id, string name) - { - Id = id; - Name = name; - } - - public override string ToString() - { - return Name; - } - - public override int GetHashCode() - { - return Id.GetHashCode(); - } - - public bool Equals(ReleaseStatus other) - { - if (ReferenceEquals(null, other)) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return Id.Equals(other.Id); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return ReferenceEquals(this, obj) || Equals(obj as ReleaseStatus); - } - - public static bool operator ==(ReleaseStatus left, ReleaseStatus right) - { - return Equals(left, right); - } - - public static bool operator !=(ReleaseStatus left, ReleaseStatus right) - { - return !Equals(left, right); - } - - public static ReleaseStatus Official => new ReleaseStatus(0, "Official"); - public static ReleaseStatus Promotion => new ReleaseStatus(1, "Promotion"); - public static ReleaseStatus Bootleg => new ReleaseStatus(2, "Bootleg"); - public static ReleaseStatus Pseudo => new ReleaseStatus(3, "Pseudo-Release"); - - public static readonly List All = new List - { - Official, - Promotion, - Bootleg, - Pseudo - }; - - public static ReleaseStatus FindById(int id) - { - if (id == 0) - { - return Official; - } - - ReleaseStatus albumType = All.FirstOrDefault(v => v.Id == id); - - if (albumType == null) - { - throw new ArgumentException(@"ID does not match a known album type", nameof(id)); - } - - return albumType; - } - - public static explicit operator ReleaseStatus(int id) - { - return FindById(id); - } - - public static explicit operator int(ReleaseStatus albumType) - { - return albumType.Id; - } - - public static explicit operator ReleaseStatus(string type) - { - var releaseStatus = All.FirstOrDefault(v => v.Name.Equals(type, StringComparison.InvariantCultureIgnoreCase)); - - if (releaseStatus == null) - { - throw new ArgumentException(@"Status does not match a known release status", nameof(type)); - } - - return releaseStatus; - } - } -} diff --git a/src/NzbDrone.Core/Music/Model/SecondaryAlbumType.cs b/src/NzbDrone.Core/Music/Model/SecondaryAlbumType.cs deleted file mode 100644 index ac9202eda..000000000 --- a/src/NzbDrone.Core/Music/Model/SecondaryAlbumType.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Music -{ - public class SecondaryAlbumType : IEmbeddedDocument, IEquatable - { - public int Id { get; set; } - public string Name { get; set; } - - public SecondaryAlbumType() - { - } - - private SecondaryAlbumType(int id, string name) - { - Id = id; - Name = name; - } - - public override string ToString() - { - return Name; - } - - public override int GetHashCode() - { - return Id.GetHashCode(); - } - - public bool Equals(SecondaryAlbumType other) - { - if (ReferenceEquals(null, other)) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return Id.Equals(other.Id); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return ReferenceEquals(this, obj) || Equals(obj as SecondaryAlbumType); - } - - public static bool operator ==(SecondaryAlbumType left, SecondaryAlbumType right) - { - return Equals(left, right); - } - - public static bool operator !=(SecondaryAlbumType left, SecondaryAlbumType right) - { - return !Equals(left, right); - } - - public static SecondaryAlbumType Studio => new SecondaryAlbumType(0, "Studio"); - public static SecondaryAlbumType Compilation => new SecondaryAlbumType(1, "Compilation"); - public static SecondaryAlbumType Soundtrack => new SecondaryAlbumType(2, "Soundtrack"); - public static SecondaryAlbumType Spokenword => new SecondaryAlbumType(3, "Spokenword"); - public static SecondaryAlbumType Interview => new SecondaryAlbumType(4, "Interview"); - public static SecondaryAlbumType Audiobook => new SecondaryAlbumType(5, "Audiobook"); - public static SecondaryAlbumType Live => new SecondaryAlbumType(6, "Live"); - public static SecondaryAlbumType Remix => new SecondaryAlbumType(7, "Remix"); - public static SecondaryAlbumType DJMix => new SecondaryAlbumType(8, "DJ-mix"); - public static SecondaryAlbumType Mixtape => new SecondaryAlbumType(9, "Mixtape/Street"); - public static SecondaryAlbumType Demo => new SecondaryAlbumType(10, "Demo"); - - public static readonly List All = new List - { - Studio, - Compilation, - Soundtrack, - Spokenword, - Interview, - Live, - Remix, - DJMix, - Mixtape, - Demo - }; - - public static SecondaryAlbumType FindById(int id) - { - if (id == 0) - { - return Studio; - } - - SecondaryAlbumType albumType = All.FirstOrDefault(v => v.Id == id); - - if (albumType == null) - { - throw new ArgumentException(@"ID does not match a known album type", nameof(id)); - } - - return albumType; - } - - public static explicit operator SecondaryAlbumType(int id) - { - return FindById(id); - } - - public static explicit operator int(SecondaryAlbumType albumType) - { - return albumType.Id; - } - - public static explicit operator SecondaryAlbumType(string type) - { - var albumType = All.FirstOrDefault(v => v.Name.Equals(type, StringComparison.InvariantCultureIgnoreCase)); - - if (albumType == null) - { - throw new ArgumentException(@"Type does not match a known album type", nameof(type)); - } - - return albumType; - } - } -} diff --git a/src/NzbDrone.Core/Music/Model/Track.cs b/src/NzbDrone.Core/Music/Model/Track.cs deleted file mode 100644 index 9c1aed1b1..000000000 --- a/src/NzbDrone.Core/Music/Model/Track.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Collections.Generic; -using Equ; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.MediaFiles; - -namespace NzbDrone.Core.Music -{ - public class Track : Entity - { - public Track() - { - OldForeignTrackIds = new List(); - OldForeignRecordingIds = new List(); - Ratings = new Ratings(); - } - - // These are model fields - public string ForeignTrackId { get; set; } - public List OldForeignTrackIds { get; set; } - public string ForeignRecordingId { get; set; } - public List OldForeignRecordingIds { get; set; } - public int AlbumReleaseId { get; set; } - public int ArtistMetadataId { get; set; } - public string TrackNumber { get; set; } - public int AbsoluteTrackNumber { get; set; } - public string Title { get; set; } - public int Duration { get; set; } - public bool Explicit { get; set; } - public Ratings Ratings { get; set; } - public int MediumNumber { get; set; } - public int TrackFileId { get; set; } - - [MemberwiseEqualityIgnore] - public bool HasFile => TrackFileId > 0; - - // These are dynamically queried from the DB - [MemberwiseEqualityIgnore] - public LazyLoaded AlbumRelease { get; set; } - [MemberwiseEqualityIgnore] - public LazyLoaded ArtistMetadata { get; set; } - [MemberwiseEqualityIgnore] - public LazyLoaded TrackFile { get; set; } - [MemberwiseEqualityIgnore] - public LazyLoaded Artist { get; set; } - - // These are retained for compatibility - // TODO: Remove set, bodged in because tests expect this to be writable - [MemberwiseEqualityIgnore] - public int AlbumId - { - get { return AlbumRelease?.Value?.Album?.Value?.Id ?? 0; } set { /* empty */ } - } - - [MemberwiseEqualityIgnore] - public Album Album { get; set; } - - public override string ToString() - { - return string.Format("[{0}]{1}", ForeignTrackId, Title.NullSafe()); - } - - public override void UseMetadataFrom(Track other) - { - ForeignTrackId = other.ForeignTrackId; - OldForeignTrackIds = other.OldForeignTrackIds; - ForeignRecordingId = other.ForeignRecordingId; - OldForeignRecordingIds = other.OldForeignRecordingIds; - TrackNumber = other.TrackNumber; - AbsoluteTrackNumber = other.AbsoluteTrackNumber; - Title = other.Title; - Duration = other.Duration; - Explicit = other.Explicit; - Ratings = other.Ratings; - MediumNumber = other.MediumNumber; - } - - public override void UseDbFieldsFrom(Track other) - { - Id = other.Id; - AlbumReleaseId = other.AlbumReleaseId; - ArtistMetadataId = other.ArtistMetadataId; - TrackFileId = other.TrackFileId; - } - } -} diff --git a/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs b/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs deleted file mode 100644 index 216606ba8..000000000 --- a/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dapper; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Music -{ - public interface IAlbumRepository : IBasicRepository - { - List GetAlbums(int artistId); - List GetLastAlbums(IEnumerable artistMetadataIds); - List GetNextAlbums(IEnumerable artistMetadataIds); - List GetAlbumsByArtistMetadataId(int artistMetadataId); - List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds); - Album FindByTitle(int artistMetadataId, string title); - Album FindById(string foreignAlbumId); - PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); - PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff); - List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored); - List ArtistAlbumsBetweenDates(Artist artist, DateTime startDate, DateTime endDate, bool includeUnmonitored); - void SetMonitoredFlat(Album album, bool monitored); - void SetMonitored(IEnumerable ids, bool monitored); - Album FindAlbumByRelease(string albumReleaseId); - Album FindAlbumByTrack(int trackId); - List GetArtistAlbumsWithFiles(Artist artist); - } - - public class AlbumRepository : BasicRepository, IAlbumRepository - { - public AlbumRepository(IMainDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - - public List GetAlbums(int artistId) - { - return Query(Builder().Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId).Where(a => a.Id == artistId)); - } - - public List GetLastAlbums(IEnumerable artistMetadataIds) - { - var now = DateTime.UtcNow; - return Query(Builder().Where(x => artistMetadataIds.Contains(x.ArtistMetadataId) && x.ReleaseDate < now) - .GroupBy(x => x.ArtistMetadataId) - .Having("Albums.ReleaseDate = MAX(Albums.ReleaseDate)")); - } - - public List GetNextAlbums(IEnumerable artistMetadataIds) - { - var now = DateTime.UtcNow; - return Query(Builder().Where(x => artistMetadataIds.Contains(x.ArtistMetadataId) && x.ReleaseDate > now) - .GroupBy(x => x.ArtistMetadataId) - .Having("Albums.ReleaseDate = MIN(Albums.ReleaseDate)")); - } - - public List GetAlbumsByArtistMetadataId(int artistMetadataId) - { - return Query(s => s.ArtistMetadataId == artistMetadataId); - } - - public List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds) - { - return Query(a => a.ArtistMetadataId == artistMetadataId || foreignIds.Contains(a.ForeignAlbumId)); - } - - public Album FindById(string foreignAlbumId) - { - return Query(s => s.ForeignAlbumId == foreignAlbumId).SingleOrDefault(); - } - - //x.Id == null is converted to SQL, so warning incorrect -#pragma warning disable CS0472 - private SqlBuilder AlbumsWithoutFilesBuilder(DateTime currentTime, bool monitored) => Builder() - .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .Join((a, r) => a.Id == r.AlbumId) - .Join((r, t) => r.Id == t.AlbumReleaseId) - .LeftJoin((t, f) => t.TrackFileId == f.Id) - .Where(f => f.Id == null) - .Where(r => r.Monitored == true) - .Where(a => a.ReleaseDate <= currentTime && a.Monitored == monitored) - .Where(a => a.Monitored == monitored) - .GroupBy(a => a.Id); -#pragma warning restore CS0472 - - public PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec) - { - var currentTime = DateTime.UtcNow; - var monitored = pagingSpec.FilterExpressions.FirstOrDefault().ToString().Contains("True"); - - pagingSpec.Records = GetPagedRecords(AlbumsWithoutFilesBuilder(currentTime, monitored), pagingSpec, PagedQuery); - pagingSpec.TotalRecords = GetPagedRecordCount(AlbumsWithoutFilesBuilder(currentTime, monitored).SelectCountDistinct(x => x.Id), pagingSpec); - - return pagingSpec; - } - - private SqlBuilder AlbumsWhereCutoffUnmetBuilder(bool monitored, List qualitiesBelowCutoff) => Builder() - .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .Join((a, r) => a.Id == r.AlbumId) - .Join((r, t) => r.Id == t.AlbumReleaseId) - .LeftJoin((t, f) => t.TrackFileId == f.Id) - .Where(r => r.Monitored == true) - .Where(a => a.Monitored == monitored) - .Where(a => a.Monitored == monitored) - .GroupBy(a => a.Id) - .Having(BuildQualityCutoffWhereClause(qualitiesBelowCutoff)); - - private string BuildQualityCutoffWhereClause(List qualitiesBelowCutoff) - { - var clauses = new List(); - - foreach (var profile in qualitiesBelowCutoff) - { - foreach (var belowCutoff in profile.QualityIds) - { - clauses.Add(string.Format("(Artists.[QualityProfileId] = {0} AND MIN(TrackFiles.Quality) LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); - } - } - - return string.Format("({0})", string.Join(" OR ", clauses)); - } - - public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff) - { - var monitored = pagingSpec.FilterExpressions.FirstOrDefault().ToString().Contains("True"); - - pagingSpec.Records = GetPagedRecords(AlbumsWhereCutoffUnmetBuilder(monitored, qualitiesBelowCutoff), pagingSpec, PagedQuery); - - var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM {TableMapping.Mapper.TableNameMapping(typeof(Album))} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/)"; - pagingSpec.TotalRecords = GetPagedRecordCount(AlbumsWhereCutoffUnmetBuilder(monitored, qualitiesBelowCutoff).Select(typeof(Album)), pagingSpec, countTemplate); - - return pagingSpec; - } - - public List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored) - { - var builder = Builder().Where(rg => rg.ReleaseDate >= startDate && rg.ReleaseDate <= endDate); - - if (!includeUnmonitored) - { - builder = builder.Where(e => e.Monitored == true) - .Where(e => e.Monitored == true); - } - - return Query(builder); - } - - public List ArtistAlbumsBetweenDates(Artist artist, DateTime startDate, DateTime endDate, bool includeUnmonitored) - { - var builder = Builder().Where(rg => rg.ReleaseDate >= startDate && - rg.ReleaseDate <= endDate && - rg.ArtistMetadataId == artist.ArtistMetadataId); - - if (!includeUnmonitored) - { - builder = builder.Where(e => e.Monitored == true) - .Where(e => e.Monitored == true); - } - - return Query(builder); - } - - public void SetMonitoredFlat(Album album, bool monitored) - { - album.Monitored = monitored; - SetFields(album, p => p.Monitored); - } - - public void SetMonitored(IEnumerable ids, bool monitored) - { - var albums = ids.Select(x => new Album { Id = x, Monitored = monitored }).ToList(); - SetFields(albums, p => p.Monitored); - } - - public Album FindByTitle(int artistMetadataId, string title) - { - var cleanTitle = Parser.Parser.CleanArtistName(title); - - if (string.IsNullOrEmpty(cleanTitle)) - { - cleanTitle = title; - } - - return Query(s => (s.CleanTitle == cleanTitle || s.Title == title) && s.ArtistMetadataId == artistMetadataId) - .ExclusiveOrDefault(); - } - - public Album FindAlbumByRelease(string albumReleaseId) - { - return Query(Builder().Join((a, r) => a.Id == r.AlbumId) - .Where(x => x.ForeignReleaseId == albumReleaseId)).FirstOrDefault(); - } - - public Album FindAlbumByTrack(int trackId) - { - return Query(Builder().Join((a, r) => a.Id == r.AlbumId) - .Join((r, t) => r.Id == t.AlbumReleaseId) - .Where(x => x.Id == trackId)).FirstOrDefault(); - } - - public List GetArtistAlbumsWithFiles(Artist artist) - { - var id = artist.ArtistMetadataId; - return Query(Builder().Join((a, r) => a.Id == r.AlbumId) - .Join((r, t) => r.Id == t.AlbumReleaseId) - .Join((t, f) => t.TrackFileId == f.Id) - .Where(x => x.ArtistMetadataId == id) - .Where(r => r.Monitored == true) - .GroupBy(x => x.Id)); - } - } -} diff --git a/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs b/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs deleted file mode 100644 index 0c7c121da..000000000 --- a/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Dapper; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Music -{ - public interface IArtistRepository : IBasicRepository - { - bool ArtistPathExists(string path); - Artist FindByName(string cleanName); - Artist FindById(string foreignArtistId); - Artist GetArtistByMetadataId(int artistMetadataId); - List GetArtistByMetadataId(IEnumerable artistMetadataId); - } - - public class ArtistRepository : BasicRepository, IArtistRepository - { - public ArtistRepository(IMainDatabase database, - IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - - protected override SqlBuilder Builder() => new SqlBuilder() - .Join((a, m) => a.ArtistMetadataId == m.Id); - - protected override List Query(SqlBuilder builder) => Query(_database, builder).ToList(); - - public static IEnumerable Query(IDatabase database, SqlBuilder builder) - { - return database.QueryJoined(builder, (artist, metadata) => - { - artist.Metadata = metadata; - return artist; - }); - } - - public bool ArtistPathExists(string path) - { - return Query(c => c.Path == path).Any(); - } - - public Artist FindById(string foreignArtistId) - { - var artist = Query(Builder().Where(m => m.ForeignArtistId == foreignArtistId)).SingleOrDefault(); - - if (artist == null) - { - artist = Query(Builder().Where(x => x.OldForeignArtistIds.Contains(foreignArtistId))).SingleOrDefault(); - } - - return artist; - } - - public Artist FindByName(string cleanName) - { - cleanName = cleanName.ToLowerInvariant(); - - return Query(s => s.CleanName == cleanName).ExclusiveOrDefault(); - } - - public Artist GetArtistByMetadataId(int artistMetadataId) - { - return Query(s => s.ArtistMetadataId == artistMetadataId).SingleOrDefault(); - } - - public List GetArtistByMetadataId(IEnumerable artistMetadataIds) - { - return Query(s => artistMetadataIds.Contains(s.ArtistMetadataId)); - } - } -} diff --git a/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs b/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs deleted file mode 100644 index 23898960d..000000000 --- a/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Dapper; -using NzbDrone.Common.EnsureThat; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Music -{ - public interface IReleaseRepository : IBasicRepository - { - AlbumRelease FindByForeignReleaseId(string foreignReleaseId, bool checkRedirect = false); - List FindByAlbum(int id); - List FindByRecordingId(List recordingIds); - List GetReleasesForRefresh(int albumId, IEnumerable foreignReleaseIds); - List SetMonitored(AlbumRelease release); - } - - public class ReleaseRepository : BasicRepository, IReleaseRepository - { - public ReleaseRepository(IMainDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - - public AlbumRelease FindByForeignReleaseId(string foreignReleaseId, bool checkRedirect = false) - { - var release = Query(x => x.ForeignReleaseId == foreignReleaseId).SingleOrDefault(); - - if (release == null && checkRedirect) - { - var id = "\"" + foreignReleaseId + "\""; - release = Query(x => x.OldForeignReleaseIds.Contains(id)).SingleOrDefault(); - } - - return release; - } - - public List GetReleasesForRefresh(int albumId, IEnumerable foreignReleaseIds) - { - return Query(r => r.AlbumId == albumId || foreignReleaseIds.Contains(r.ForeignReleaseId)); - } - - public List FindByAlbum(int id) - { - // populate the albums and artist metadata also - // this hopefully speeds up the track matching a lot - var builder = new SqlBuilder() - .Join((r, a) => r.AlbumId == a.Id) - .Join((a, m) => a.ArtistMetadataId == m.Id) - .Where(r => r.AlbumId == id); - - return _database.QueryJoined(builder, (release, album, metadata) => - { - release.Album = album; - release.Album.Value.ArtistMetadata = metadata; - return release; - }).ToList(); - } - - public List FindByRecordingId(List recordingIds) - { - return Query(Builder() - .Join((r, t) => r.Id == t.AlbumReleaseId) - .Where(t => Enumerable.Contains(recordingIds, t.ForeignRecordingId)) - .GroupBy(x => x.Id)); - } - - public List SetMonitored(AlbumRelease release) - { - var allReleases = FindByAlbum(release.AlbumId); - allReleases.ForEach(r => r.Monitored = r.Id == release.Id); - Ensure.That(allReleases.Count(x => x.Monitored) == 1).IsTrue(); - UpdateMany(allReleases); - return allReleases; - } - } -} diff --git a/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs b/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs deleted file mode 100644 index ce5906378..000000000 --- a/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Music -{ - public interface ITrackRepository : IBasicRepository - { - List GetTracks(int artistId); - List GetTracksByAlbum(int albumId); - List GetTracksByRelease(int albumReleaseId); - List GetTracksByReleases(List albumReleaseIds); - List GetTracksForRefresh(int albumReleaseId, IEnumerable foreignTrackIds); - List GetTracksByFileId(int fileId); - List GetTracksByFileId(IEnumerable ids); - List TracksWithFiles(int artistId); - List TracksWithoutFiles(int albumId); - void SetFileId(List tracks); - void DetachTrackFile(int trackFileId); - } - - public class TrackRepository : BasicRepository, ITrackRepository - { - public TrackRepository(IMainDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - - public List GetTracks(int artistId) - { - return Query(Builder() - .Join((t, r) => t.AlbumReleaseId == r.Id) - .Join((r, a) => r.AlbumId == a.Id) - .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) - .Where(r => r.Monitored == true) - .Where(x => x.Id == artistId)); - } - - public List GetTracksByAlbum(int albumId) - { - return Query(Builder() - .Join((t, r) => t.AlbumReleaseId == r.Id) - .Join((r, a) => r.AlbumId == a.Id) - .Where(r => r.Monitored == true) - .Where(x => x.Id == albumId)); - } - - public List GetTracksByRelease(int albumReleaseId) - { - return Query(t => t.AlbumReleaseId == albumReleaseId).ToList(); - } - - public List GetTracksByReleases(List albumReleaseIds) - { - // this will populate the artist metadata also - return _database.QueryJoined(Builder() - .Join((l, r) => l.ArtistMetadataId == r.Id) - .Where(x => albumReleaseIds.Contains(x.AlbumReleaseId)), (track, metadata) => - { - track.ArtistMetadata = metadata; - return track; - }).ToList(); - } - - public List GetTracksForRefresh(int albumReleaseId, IEnumerable foreignTrackIds) - { - return Query(a => a.AlbumReleaseId == albumReleaseId || foreignTrackIds.Contains(a.ForeignTrackId)); - } - - public List GetTracksByFileId(int fileId) - { - return Query(e => e.TrackFileId == fileId); - } - - public List GetTracksByFileId(IEnumerable ids) - { - return Query(x => ids.Contains(x.TrackFileId)); - } - - public List TracksWithFiles(int artistId) - { - return Query(Builder() - .Join((t, r) => t.AlbumReleaseId == r.Id) - .Join((r, a) => r.AlbumId == a.Id) - .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) - .Join((t, f) => t.TrackFileId == f.Id) - .Where(r => r.Monitored == true) - .Where(x => x.Id == artistId)); - } - - public List TracksWithoutFiles(int albumId) - { - //x.Id == null is converted to SQL, so warning incorrect -#pragma warning disable CS0472 - return Query(Builder() - .Join((t, r) => t.AlbumReleaseId == r.Id) - .LeftJoin((t, f) => t.TrackFileId == f.Id) - .Where(r => r.Monitored == true && r.AlbumId == albumId) - .Where(x => x.Id == null)); -#pragma warning restore CS0472 - } - - public void SetFileId(List tracks) - { - SetFields(tracks, t => t.TrackFileId); - } - - public void DetachTrackFile(int trackFileId) - { - var tracks = GetTracksByFileId(trackFileId); - tracks.ForEach(x => x.TrackFileId = 0); - SetFileId(tracks); - } - } -} diff --git a/src/NzbDrone.Core/Music/Services/AlbumEditedService.cs b/src/NzbDrone.Core/Music/Services/AlbumEditedService.cs deleted file mode 100644 index 13cccc4a5..000000000 --- a/src/NzbDrone.Core/Music/Services/AlbumEditedService.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Commands; -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Music.Events; - -namespace NzbDrone.Core.Music -{ - public class AlbumEditedService : IHandle - { - private readonly IManageCommandQueue _commandQueueManager; - private readonly ITrackService _trackService; - - public AlbumEditedService(IManageCommandQueue commandQueueManager, - ITrackService trackService) - { - _commandQueueManager = commandQueueManager; - _trackService = trackService; - } - - public void Handle(AlbumEditedEvent message) - { - if (message.Album.AlbumReleases.IsLoaded && message.OldAlbum.AlbumReleases.IsLoaded) - { - var new_monitored = new HashSet(message.Album.AlbumReleases.Value.Where(x => x.Monitored).Select(x => x.Id)); - var old_monitored = new HashSet(message.OldAlbum.AlbumReleases.Value.Where(x => x.Monitored).Select(x => x.Id)); - if (!new_monitored.SetEquals(old_monitored) || - (!message.OldAlbum.AnyReleaseOk && message.Album.AnyReleaseOk)) - { - // Unlink any old track files - var tracks = _trackService.GetTracksByAlbum(message.Album.Id); - tracks.ForEach(x => x.TrackFileId = 0); - _trackService.SetFileIds(tracks); - - _commandQueueManager.Push(new RescanFoldersCommand(null, FilterFilesType.Matched, false, null)); - } - } - } - } -} diff --git a/src/NzbDrone.Core/Music/Services/RefreshAlbumReleaseService.cs b/src/NzbDrone.Core/Music/Services/RefreshAlbumReleaseService.cs deleted file mode 100644 index 294610a35..000000000 --- a/src/NzbDrone.Core/Music/Services/RefreshAlbumReleaseService.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Core.Music -{ - public interface IRefreshAlbumReleaseService - { - bool RefreshEntityInfo(AlbumRelease entity, List remoteEntityList, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate); - bool RefreshEntityInfo(List releases, List remoteEntityList, bool forceChildRefresh, bool forceUpdateFileTags); - } - - public class RefreshAlbumReleaseService : RefreshEntityServiceBase, IRefreshAlbumReleaseService - { - private readonly IReleaseService _releaseService; - private readonly IRefreshTrackService _refreshTrackService; - private readonly ITrackService _trackService; - - public RefreshAlbumReleaseService(IReleaseService releaseService, - IArtistMetadataService artistMetadataService, - IRefreshTrackService refreshTrackService, - ITrackService trackService, - Logger logger) - : base(logger, artistMetadataService) - { - _releaseService = releaseService; - _trackService = trackService; - _refreshTrackService = refreshTrackService; - } - - protected override RemoteData GetRemoteData(AlbumRelease local, List remote) - { - var result = new RemoteData(); - result.Entity = remote.SingleOrDefault(x => x.ForeignReleaseId == local.ForeignReleaseId || x.OldForeignReleaseIds.Contains(local.ForeignReleaseId)); - return result; - } - - protected override bool IsMerge(AlbumRelease local, AlbumRelease remote) - { - return local.ForeignReleaseId != remote.ForeignReleaseId; - } - - protected override UpdateResult UpdateEntity(AlbumRelease local, AlbumRelease remote) - { - if (local.Equals(remote)) - { - return UpdateResult.None; - } - - local.UseMetadataFrom(remote); - - return UpdateResult.UpdateTags; - } - - protected override AlbumRelease GetEntityByForeignId(AlbumRelease local) - { - return _releaseService.GetReleaseByForeignReleaseId(local.ForeignReleaseId); - } - - protected override void SaveEntity(AlbumRelease local) - { - _releaseService.UpdateMany(new List { local }); - } - - protected override void DeleteEntity(AlbumRelease local, bool deleteFiles) - { - _releaseService.DeleteMany(new List { local }); - } - - protected override List GetRemoteChildren(AlbumRelease remote) - { - return remote.Tracks.Value.DistinctBy(m => m.ForeignTrackId).ToList(); - } - - protected override List GetLocalChildren(AlbumRelease entity, List remoteChildren) - { - return _trackService.GetTracksForRefresh(entity.Id, - remoteChildren.Select(x => x.ForeignTrackId) - .Concat(remoteChildren.SelectMany(x => x.OldForeignTrackIds))); - } - - protected override Tuple> GetMatchingExistingChildren(List existingChildren, Track remote) - { - var existingChild = existingChildren.SingleOrDefault(x => x.ForeignTrackId == remote.ForeignTrackId); - var mergeChildren = existingChildren.Where(x => remote.OldForeignTrackIds.Contains(x.ForeignTrackId)).ToList(); - return Tuple.Create(existingChild, mergeChildren); - } - - protected override void PrepareNewChild(Track child, AlbumRelease entity) - { - child.AlbumReleaseId = entity.Id; - child.AlbumRelease = entity; - child.ArtistMetadataId = child.ArtistMetadata.Value.Id; - - // make sure title is not null - child.Title = child.Title ?? "Unknown"; - } - - protected override void PrepareExistingChild(Track local, Track remote, AlbumRelease entity) - { - local.AlbumRelease = entity; - local.AlbumReleaseId = entity.Id; - local.ArtistMetadataId = remote.ArtistMetadata.Value.Id; - remote.Id = local.Id; - remote.TrackFileId = local.TrackFileId; - remote.AlbumReleaseId = local.AlbumReleaseId; - remote.ArtistMetadataId = local.ArtistMetadataId; - } - - protected override void AddChildren(List children) - { - _trackService.InsertMany(children); - } - - protected override bool RefreshChildren(SortedChildren localChildren, List remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) - { - return _refreshTrackService.RefreshTrackInfo(localChildren.Added, localChildren.Updated, localChildren.Merged, localChildren.Deleted, localChildren.UpToDate, remoteChildren, forceUpdateFileTags); - } - } -} diff --git a/src/NzbDrone.Core/Music/Services/RefreshAlbumService.cs b/src/NzbDrone.Core/Music/Services/RefreshAlbumService.cs deleted file mode 100644 index 2c536ffa9..000000000 --- a/src/NzbDrone.Core/Music/Services/RefreshAlbumService.cs +++ /dev/null @@ -1,365 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Instrumentation.Extensions; -using NzbDrone.Core.Exceptions; -using NzbDrone.Core.History; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.MetadataSource; -using NzbDrone.Core.Music.Commands; -using NzbDrone.Core.Music.Events; - -namespace NzbDrone.Core.Music -{ - public interface IRefreshAlbumService - { - bool RefreshAlbumInfo(Album album, List remoteAlbums, bool forceUpdateFileTags); - bool RefreshAlbumInfo(List albums, List remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate); - } - - public class RefreshAlbumService : RefreshEntityServiceBase, IRefreshAlbumService, IExecute - { - private readonly IAlbumService _albumService; - private readonly IArtistService _artistService; - private readonly IAddArtistService _addArtistService; - private readonly IReleaseService _releaseService; - private readonly IProvideAlbumInfo _albumInfo; - private readonly IRefreshAlbumReleaseService _refreshAlbumReleaseService; - private readonly IMediaFileService _mediaFileService; - private readonly IHistoryService _historyService; - private readonly IEventAggregator _eventAggregator; - private readonly ICheckIfAlbumShouldBeRefreshed _checkIfAlbumShouldBeRefreshed; - private readonly IMapCoversToLocal _mediaCoverService; - private readonly Logger _logger; - - public RefreshAlbumService(IAlbumService albumService, - IArtistService artistService, - IAddArtistService addArtistService, - IArtistMetadataService artistMetadataService, - IReleaseService releaseService, - IProvideAlbumInfo albumInfo, - IRefreshAlbumReleaseService refreshAlbumReleaseService, - IMediaFileService mediaFileService, - IHistoryService historyService, - IEventAggregator eventAggregator, - ICheckIfAlbumShouldBeRefreshed checkIfAlbumShouldBeRefreshed, - IMapCoversToLocal mediaCoverService, - Logger logger) - : base(logger, artistMetadataService) - { - _albumService = albumService; - _artistService = artistService; - _addArtistService = addArtistService; - _releaseService = releaseService; - _albumInfo = albumInfo; - _refreshAlbumReleaseService = refreshAlbumReleaseService; - _mediaFileService = mediaFileService; - _historyService = historyService; - _eventAggregator = eventAggregator; - _checkIfAlbumShouldBeRefreshed = checkIfAlbumShouldBeRefreshed; - _mediaCoverService = mediaCoverService; - _logger = logger; - } - - protected override RemoteData GetRemoteData(Album local, List remote) - { - var result = new RemoteData(); - - // remove not in remote list and ShouldDelete is true - if (remote != null && - !remote.Any(x => x.ForeignAlbumId == local.ForeignAlbumId || x.OldForeignAlbumIds.Contains(local.ForeignAlbumId)) && - ShouldDelete(local)) - { - return result; - } - - Tuple> tuple = null; - try - { - tuple = _albumInfo.GetAlbumInfo(local.ForeignAlbumId); - } - catch (AlbumNotFoundException) - { - return result; - } - - if (tuple.Item2.AlbumReleases.Value.Count == 0) - { - _logger.Debug($"{local} has no valid releases, removing."); - return result; - } - - result.Entity = tuple.Item2; - result.Entity.Id = local.Id; - result.Metadata = tuple.Item3; - return result; - } - - protected override void EnsureNewParent(Album local, Album remote) - { - // Make sure the appropriate artist exists (it could be that an album changes parent) - // The artistMetadata entry will be in the db but make sure a corresponding artist is too - // so that the album doesn't just disappear. - - // TODO filter by metadata id before hitting database - _logger.Trace($"Ensuring parent artist exists [{remote.ArtistMetadata.Value.ForeignArtistId}]"); - - var newArtist = _artistService.FindById(remote.ArtistMetadata.Value.ForeignArtistId); - - if (newArtist == null) - { - var oldArtist = local.Artist.Value; - var addArtist = new Artist - { - Metadata = remote.ArtistMetadata.Value, - MetadataProfileId = oldArtist.MetadataProfileId, - QualityProfileId = oldArtist.QualityProfileId, - RootFolderPath = oldArtist.RootFolderPath, - Monitored = oldArtist.Monitored, - AlbumFolder = oldArtist.AlbumFolder, - Tags = oldArtist.Tags - }; - _logger.Debug($"Adding missing parent artist {addArtist}"); - _addArtistService.AddArtist(addArtist); - } - } - - protected override bool ShouldDelete(Album local) - { - // not manually added and has no files - return local.AddOptions.AddType != AlbumAddType.Manual && - !_mediaFileService.GetFilesByAlbum(local.Id).Any(); - } - - protected override void LogProgress(Album local) - { - _logger.ProgressInfo("Updating Info for {0}", local.Title); - } - - protected override bool IsMerge(Album local, Album remote) - { - return local.ForeignAlbumId != remote.ForeignAlbumId; - } - - protected override UpdateResult UpdateEntity(Album local, Album remote) - { - UpdateResult result; - - remote.UseDbFieldsFrom(local); - - if (local.Title != (remote.Title ?? "Unknown") || - local.ForeignAlbumId != remote.ForeignAlbumId || - local.ArtistMetadata.Value.ForeignArtistId != remote.ArtistMetadata.Value.ForeignArtistId) - { - result = UpdateResult.UpdateTags; - } - else if (!local.Equals(remote)) - { - result = UpdateResult.Standard; - } - else - { - result = UpdateResult.None; - } - - // Force update and fetch covers if images have changed so that we can write them into tags - if (remote.Images.Any() && !local.Images.SequenceEqual(remote.Images)) - { - _mediaCoverService.EnsureAlbumCovers(remote); - result = UpdateResult.UpdateTags; - } - - local.UseMetadataFrom(remote); - - local.ArtistMetadataId = remote.ArtistMetadata.Value.Id; - local.LastInfoSync = DateTime.UtcNow; - local.AlbumReleases = new List(); - - return result; - } - - protected override UpdateResult MergeEntity(Album local, Album target, Album remote) - { - _logger.Warn($"Album {local} was merged with {remote} because the original was a duplicate."); - - // move releases over to the new album and delete - var localReleases = _releaseService.GetReleasesByAlbum(local.Id); - var allReleases = localReleases.Concat(_releaseService.GetReleasesByAlbum(target.Id)).ToList(); - _logger.Trace($"Moving {localReleases.Count} releases from {local} to {remote}"); - - // Update album ID and unmonitor all releases from the old album - allReleases.ForEach(x => x.AlbumId = target.Id); - MonitorSingleRelease(allReleases); - _releaseService.UpdateMany(allReleases); - - // Update album ids for trackfiles - var files = _mediaFileService.GetFilesByAlbum(local.Id); - files.ForEach(x => x.AlbumId = target.Id); - _mediaFileService.Update(files); - - // Update album ids for history - var items = _historyService.GetByAlbum(local.Id, null); - items.ForEach(x => x.AlbumId = target.Id); - _historyService.UpdateMany(items); - - // Finally delete the old album - _albumService.DeleteMany(new List { local }); - - return UpdateResult.UpdateTags; - } - - protected override Album GetEntityByForeignId(Album local) - { - return _albumService.FindById(local.ForeignAlbumId); - } - - protected override void SaveEntity(Album local) - { - // Use UpdateMany to avoid firing the album edited event - _albumService.UpdateMany(new List { local }); - } - - protected override void DeleteEntity(Album local, bool deleteFiles) - { - _albumService.DeleteAlbum(local.Id, true); - } - - protected override List GetRemoteChildren(Album remote) - { - return remote.AlbumReleases.Value.DistinctBy(m => m.ForeignReleaseId).ToList(); - } - - protected override List GetLocalChildren(Album entity, List remoteChildren) - { - var children = _releaseService.GetReleasesForRefresh(entity.Id, - remoteChildren.Select(x => x.ForeignReleaseId) - .Concat(remoteChildren.SelectMany(x => x.OldForeignReleaseIds))); - - // Make sure trackfiles point to the new album where we are grabbing a release from another album - var files = new List(); - foreach (var release in children.Where(x => x.AlbumId != entity.Id)) - { - files.AddRange(_mediaFileService.GetFilesByRelease(release.Id)); - } - - files.ForEach(x => x.AlbumId = entity.Id); - _mediaFileService.Update(files); - - return children; - } - - protected override Tuple> GetMatchingExistingChildren(List existingChildren, AlbumRelease remote) - { - var existingChild = existingChildren.SingleOrDefault(x => x.ForeignReleaseId == remote.ForeignReleaseId); - var mergeChildren = existingChildren.Where(x => remote.OldForeignReleaseIds.Contains(x.ForeignReleaseId)).ToList(); - return Tuple.Create(existingChild, mergeChildren); - } - - protected override void PrepareNewChild(AlbumRelease child, Album entity) - { - child.AlbumId = entity.Id; - child.Album = entity; - } - - protected override void PrepareExistingChild(AlbumRelease local, AlbumRelease remote, Album entity) - { - local.AlbumId = entity.Id; - local.Album = entity; - - remote.UseDbFieldsFrom(local); - } - - protected override void AddChildren(List children) - { - _releaseService.InsertMany(children); - } - - private void MonitorSingleRelease(List releases) - { - var monitored = releases.Where(x => x.Monitored).ToList(); - if (!monitored.Any()) - { - monitored = releases; - } - - var toMonitor = monitored.OrderByDescending(x => _mediaFileService.GetFilesByRelease(x.Id).Count) - .ThenByDescending(x => x.TrackCount) - .First(); - - releases.ForEach(x => x.Monitored = false); - toMonitor.Monitored = true; - } - - protected override bool RefreshChildren(SortedChildren localChildren, List remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) - { - var refreshList = localChildren.All; - - // make sure only one of the releases ends up monitored - localChildren.Old.ForEach(x => x.Monitored = false); - MonitorSingleRelease(localChildren.Future); - - refreshList.ForEach(x => _logger.Trace($"release: {x} monitored: {x.Monitored}")); - - return _refreshAlbumReleaseService.RefreshEntityInfo(refreshList, remoteChildren, forceChildRefresh, forceUpdateFileTags); - } - - protected override void PublishEntityUpdatedEvent(Album entity) - { - // Fetch fresh from DB so all lazy loads are available - _eventAggregator.PublishEvent(new AlbumUpdatedEvent(_albumService.GetAlbum(entity.Id))); - } - - public bool RefreshAlbumInfo(List albums, List remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) - { - bool updated = false; - - HashSet updatedMusicbrainzAlbums = null; - - if (lastUpdate.HasValue && lastUpdate.Value.AddDays(14) > DateTime.UtcNow) - { - updatedMusicbrainzAlbums = _albumInfo.GetChangedAlbums(lastUpdate.Value); - } - - foreach (var album in albums) - { - if (forceAlbumRefresh || - (updatedMusicbrainzAlbums == null && _checkIfAlbumShouldBeRefreshed.ShouldRefresh(album)) || - (updatedMusicbrainzAlbums != null && updatedMusicbrainzAlbums.Contains(album.ForeignAlbumId))) - { - updated |= RefreshAlbumInfo(album, remoteAlbums, forceUpdateFileTags); - } - else - { - _logger.Debug("Skipping refresh of album: {0}", album.Title); - } - } - - return updated; - } - - public bool RefreshAlbumInfo(Album album, List remoteAlbums, bool forceUpdateFileTags) - { - return RefreshEntityInfo(album, remoteAlbums, true, forceUpdateFileTags, null); - } - - public void Execute(RefreshAlbumCommand message) - { - if (message.AlbumId.HasValue) - { - var album = _albumService.GetAlbum(message.AlbumId.Value); - var artist = _artistService.GetArtistByMetadataId(album.ArtistMetadataId); - var updated = RefreshAlbumInfo(album, null, false); - if (updated) - { - _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); - _eventAggregator.PublishEvent(new AlbumUpdatedEvent(album)); - } - } - } - } -} diff --git a/src/NzbDrone.Core/Music/Services/RefreshTrackService.cs b/src/NzbDrone.Core/Music/Services/RefreshTrackService.cs deleted file mode 100644 index 947e4bec2..000000000 --- a/src/NzbDrone.Core/Music/Services/RefreshTrackService.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Core.MediaFiles; - -namespace NzbDrone.Core.Music -{ - public interface IRefreshTrackService - { - bool RefreshTrackInfo(List add, List update, List> merge, List delete, List upToDate, List remoteTracks, bool forceUpdateFileTags); - } - - public class RefreshTrackService : IRefreshTrackService - { - private readonly ITrackService _trackService; - private readonly IAudioTagService _audioTagService; - private readonly Logger _logger; - - public RefreshTrackService(ITrackService trackService, - IAudioTagService audioTagService, - Logger logger) - { - _trackService = trackService; - _audioTagService = audioTagService; - _logger = logger; - } - - public bool RefreshTrackInfo(List add, List update, List> merge, List delete, List upToDate, List remoteTracks, bool forceUpdateFileTags) - { - var updateList = new List(); - - // for tracks that need updating, just grab the remote track and set db ids - foreach (var track in update) - { - var remoteTrack = remoteTracks.Single(e => e.ForeignTrackId == track.ForeignTrackId); - track.UseMetadataFrom(remoteTrack); - - // make sure title is not null - track.Title = track.Title ?? "Unknown"; - updateList.Add(track); - } - - // Move trackfiles from merged entities into new one - foreach (var item in merge) - { - var trackToMerge = item.Item1; - var mergeTarget = item.Item2; - - if (mergeTarget.TrackFileId == 0) - { - mergeTarget.TrackFileId = trackToMerge.TrackFileId; - } - - if (!updateList.Contains(mergeTarget)) - { - updateList.Add(mergeTarget); - } - } - - _trackService.DeleteMany(delete.Concat(merge.Select(x => x.Item1)).ToList()); - _trackService.UpdateMany(updateList); - - var tagsToUpdate = updateList; - if (forceUpdateFileTags) - { - _logger.Debug("Forcing tag update due to Artist/Album/Release updates"); - tagsToUpdate = updateList.Concat(upToDate).ToList(); - } - - _audioTagService.SyncTags(tagsToUpdate); - - return add.Any() || delete.Any() || updateList.Any() || merge.Any(); - } - } -} diff --git a/src/NzbDrone.Core/Music/Services/ReleaseService.cs b/src/NzbDrone.Core/Music/Services/ReleaseService.cs deleted file mode 100644 index 69b33613c..000000000 --- a/src/NzbDrone.Core/Music/Services/ReleaseService.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Music.Events; - -namespace NzbDrone.Core.Music -{ - public interface IReleaseService - { - AlbumRelease GetRelease(int id); - AlbumRelease GetReleaseByForeignReleaseId(string foreignReleaseId, bool checkRedirect = false); - List GetAllReleases(); - void InsertMany(List releases); - void UpdateMany(List releases); - void DeleteMany(List releases); - List GetReleasesForRefresh(int albumId, IEnumerable foreignReleaseIds); - List GetReleasesByAlbum(int releaseGroupId); - List GetReleasesByRecordingIds(List recordingIds); - List SetMonitored(AlbumRelease release); - } - - public class ReleaseService : IReleaseService, - IHandle - { - private readonly IReleaseRepository _releaseRepository; - private readonly IEventAggregator _eventAggregator; - - public ReleaseService(IReleaseRepository releaseRepository, - IEventAggregator eventAggregator) - { - _releaseRepository = releaseRepository; - _eventAggregator = eventAggregator; - } - - public AlbumRelease GetRelease(int id) - { - return _releaseRepository.Get(id); - } - - public AlbumRelease GetReleaseByForeignReleaseId(string foreignReleaseId, bool checkRedirect = false) - { - return _releaseRepository.FindByForeignReleaseId(foreignReleaseId, checkRedirect); - } - - public List GetAllReleases() - { - return _releaseRepository.All().ToList(); - } - - public void InsertMany(List releases) - { - _releaseRepository.InsertMany(releases); - } - - public void UpdateMany(List releases) - { - _releaseRepository.UpdateMany(releases); - } - - public void DeleteMany(List releases) - { - _releaseRepository.DeleteMany(releases); - foreach (var release in releases) - { - _eventAggregator.PublishEvent(new ReleaseDeletedEvent(release)); - } - } - - public List GetReleasesForRefresh(int albumId, IEnumerable foreignReleaseIds) - { - return _releaseRepository.GetReleasesForRefresh(albumId, foreignReleaseIds); - } - - public List GetReleasesByAlbum(int releaseGroupId) - { - return _releaseRepository.FindByAlbum(releaseGroupId); - } - - public List GetReleasesByRecordingIds(List recordingIds) - { - return _releaseRepository.FindByRecordingId(recordingIds); - } - - public List SetMonitored(AlbumRelease release) - { - return _releaseRepository.SetMonitored(release); - } - - public void Handle(AlbumDeletedEvent message) - { - var releases = GetReleasesByAlbum(message.Album.Id); - DeleteMany(releases); - } - } -} diff --git a/src/NzbDrone.Core/Music/Services/TrackService.cs b/src/NzbDrone.Core/Music/Services/TrackService.cs deleted file mode 100644 index 50c4d8891..000000000 --- a/src/NzbDrone.Core/Music/Services/TrackService.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Music.Events; - -namespace NzbDrone.Core.Music -{ - public interface ITrackService - { - Track GetTrack(int id); - List GetTracks(IEnumerable ids); - List GetTracksByArtist(int artistId); - List GetTracksByAlbum(int albumId); - List GetTracksByRelease(int albumReleaseId); - List GetTracksByReleases(List albumReleaseIds); - List GetTracksForRefresh(int albumReleaseId, IEnumerable foreignTrackIds); - List TracksWithFiles(int artistId); - List TracksWithoutFiles(int albumId); - List GetTracksByFileId(int trackFileId); - List GetTracksByFileId(IEnumerable trackFileIds); - void UpdateTrack(Track track); - void InsertMany(List tracks); - void UpdateMany(List tracks); - void DeleteMany(List tracks); - void SetFileIds(List tracks); - } - - public class TrackService : ITrackService, - IHandle, - IHandle - { - private readonly ITrackRepository _trackRepository; - private readonly Logger _logger; - - public TrackService(ITrackRepository trackRepository, - Logger logger) - { - _trackRepository = trackRepository; - _logger = logger; - } - - public Track GetTrack(int id) - { - return _trackRepository.Get(id); - } - - public List GetTracks(IEnumerable ids) - { - return _trackRepository.Get(ids).ToList(); - } - - public List GetTracksByArtist(int artistId) - { - _logger.Debug("Getting Tracks for ArtistId {0}", artistId); - return _trackRepository.GetTracks(artistId).ToList(); - } - - public List GetTracksByAlbum(int albumId) - { - return _trackRepository.GetTracksByAlbum(albumId); - } - - public List GetTracksByRelease(int albumReleaseId) - { - return _trackRepository.GetTracksByRelease(albumReleaseId); - } - - public List GetTracksByReleases(List albumReleaseIds) - { - return _trackRepository.GetTracksByReleases(albumReleaseIds); - } - - public List GetTracksForRefresh(int albumReleaseId, IEnumerable foreignTrackIds) - { - return _trackRepository.GetTracksForRefresh(albumReleaseId, foreignTrackIds); - } - - public List TracksWithFiles(int artistId) - { - return _trackRepository.TracksWithFiles(artistId); - } - - public List TracksWithoutFiles(int albumId) - { - return _trackRepository.TracksWithoutFiles(albumId); - } - - public List GetTracksByFileId(int trackFileId) - { - return _trackRepository.GetTracksByFileId(trackFileId); - } - - public List GetTracksByFileId(IEnumerable trackFileIds) - { - return _trackRepository.GetTracksByFileId(trackFileIds); - } - - public void UpdateTrack(Track track) - { - _trackRepository.Update(track); - } - - public void InsertMany(List tracks) - { - _trackRepository.InsertMany(tracks); - } - - public void UpdateMany(List tracks) - { - _trackRepository.UpdateMany(tracks); - } - - public void DeleteMany(List tracks) - { - _trackRepository.DeleteMany(tracks); - } - - public void SetFileIds(List tracks) - { - _trackRepository.SetFileId(tracks); - } - - public void Handle(ReleaseDeletedEvent message) - { - var tracks = GetTracksByRelease(message.Release.Id); - _trackRepository.DeleteMany(tracks); - } - - public void Handle(TrackFileDeletedEvent message) - { - _logger.Debug($"Detaching tracks from file {message.TrackFile}"); - _trackRepository.DetachTrackFile(message.TrackFile.Id); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/AlbumDownloadMessage.cs b/src/NzbDrone.Core/Notifications/AlbumDownloadMessage.cs index f7781d98a..6dbad8ccf 100644 --- a/src/NzbDrone.Core/Notifications/AlbumDownloadMessage.cs +++ b/src/NzbDrone.Core/Notifications/AlbumDownloadMessage.cs @@ -7,11 +7,10 @@ namespace NzbDrone.Core.Notifications public class AlbumDownloadMessage { public string Message { get; set; } - public Artist Artist { get; set; } - public Album Album { get; set; } - public AlbumRelease Release { get; set; } - public List TrackFiles { get; set; } - public List OldFiles { get; set; } + public Author Artist { get; set; } + public Book Album { get; set; } + public List TrackFiles { get; set; } + public List OldFiles { get; set; } public string DownloadClient { get; set; } public string DownloadId { get; set; } diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index c102eccf3..2b0819566 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Readarr_EventType", "Grab"); environmentVariables.Add("Readarr_Artist_Id", artist.Id.ToString()); environmentVariables.Add("Readarr_Artist_Name", artist.Metadata.Value.Name); - environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignArtistId); + environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignAuthorId); environmentVariables.Add("Readarr_Artist_Type", artist.Metadata.Value.Type); environmentVariables.Add("Readarr_Release_AlbumCount", remoteAlbum.Albums.Count.ToString()); environmentVariables.Add("Readarr_Release_AlbumReleaseDates", string.Join(",", remoteAlbum.Albums.Select(e => e.ReleaseDate))); @@ -63,19 +63,17 @@ namespace NzbDrone.Core.Notifications.CustomScript { var artist = message.Artist; var album = message.Album; - var release = message.Release; var environmentVariables = new StringDictionary(); environmentVariables.Add("Readarr_EventType", "AlbumDownload"); environmentVariables.Add("Readarr_Artist_Id", artist.Id.ToString()); environmentVariables.Add("Readarr_Artist_Name", artist.Metadata.Value.Name); environmentVariables.Add("Readarr_Artist_Path", artist.Path); - environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignArtistId); + environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignAuthorId); environmentVariables.Add("Readarr_Artist_Type", artist.Metadata.Value.Type); environmentVariables.Add("Readarr_Album_Id", album.Id.ToString()); environmentVariables.Add("Readarr_Album_Title", album.Title); - environmentVariables.Add("Readarr_Album_MBId", album.ForeignAlbumId); - environmentVariables.Add("Readarr_AlbumRelease_MBId", release.ForeignReleaseId); + environmentVariables.Add("Readarr_Album_MBId", album.ForeignBookId); environmentVariables.Add("Readarr_Album_ReleaseDate", album.ReleaseDate.ToString()); environmentVariables.Add("Readarr_Download_Client", message.DownloadClient ?? string.Empty); environmentVariables.Add("Readarr_Download_Id", message.DownloadId ?? string.Empty); @@ -93,7 +91,7 @@ namespace NzbDrone.Core.Notifications.CustomScript ExecuteScript(environmentVariables); } - public override void OnRename(Artist artist) + public override void OnRename(Author artist) { var environmentVariables = new StringDictionary(); @@ -101,7 +99,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Readarr_Artist_Id", artist.Id.ToString()); environmentVariables.Add("Readarr_Artist_Name", artist.Metadata.Value.Name); environmentVariables.Add("Readarr_Artist_Path", artist.Path); - environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignArtistId); + environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignAuthorId); environmentVariables.Add("Readarr_Artist_Type", artist.Metadata.Value.Type); ExecuteScript(environmentVariables); @@ -111,7 +109,6 @@ namespace NzbDrone.Core.Notifications.CustomScript { var artist = message.Artist; var album = message.Album; - var release = message.Release; var trackFile = message.TrackFile; var environmentVariables = new StringDictionary(); @@ -119,18 +116,14 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Readarr_Artist_Id", artist.Id.ToString()); environmentVariables.Add("Readarr_Artist_Name", artist.Metadata.Value.Name); environmentVariables.Add("Readarr_Artist_Path", artist.Path); - environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignArtistId); + environmentVariables.Add("Readarr_Artist_MBId", artist.Metadata.Value.ForeignAuthorId); environmentVariables.Add("Readarr_Artist_Type", artist.Metadata.Value.Type); environmentVariables.Add("Readarr_Album_Id", album.Id.ToString()); environmentVariables.Add("Readarr_Album_Title", album.Title); - environmentVariables.Add("Readarr_Album_MBId", album.ForeignAlbumId); - environmentVariables.Add("Readarr_AlbumRelease_MBId", release.ForeignReleaseId); + environmentVariables.Add("Readarr_Album_MBId", album.ForeignBookId); environmentVariables.Add("Readarr_Album_ReleaseDate", album.ReleaseDate.ToString()); environmentVariables.Add("Readarr_TrackFile_Id", trackFile.Id.ToString()); - environmentVariables.Add("Readarr_TrackFile_TrackCount", trackFile.Tracks.Value.Count.ToString()); environmentVariables.Add("Readarr_TrackFile_Path", trackFile.Path); - environmentVariables.Add("Readarr_TrackFile_TrackNumbers", string.Join(",", trackFile.Tracks.Value.Select(e => e.TrackNumber))); - environmentVariables.Add("Readarr_TrackFile_TrackTitles", string.Join("|", trackFile.Tracks.Value.Select(e => e.Title))); environmentVariables.Add("Readarr_TrackFile_Quality", trackFile.Quality.Quality.Name); environmentVariables.Add("Readarr_TrackFile_QualityVersion", trackFile.Quality.Revision.Version.ToString()); environmentVariables.Add("Readarr_TrackFile_ReleaseGroup", trackFile.ReleaseGroup ?? string.Empty); diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 179251cd7..395890914 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Notifications.Discord _proxy.SendPayload(payload, Settings); } - public override void OnRename(Artist artist) + public override void OnRename(Author artist) { var attachments = new List { diff --git a/src/NzbDrone.Core/Notifications/GrabMessage.cs b/src/NzbDrone.Core/Notifications/GrabMessage.cs index 8b0974b9c..864f8d063 100644 --- a/src/NzbDrone.Core/Notifications/GrabMessage.cs +++ b/src/NzbDrone.Core/Notifications/GrabMessage.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Notifications public class GrabMessage { public string Message { get; set; } - public Artist Artist { get; set; } + public Author Artist { get; set; } public RemoteAlbum Album { get; set; } public QualityModel Quality { get; set; } public string DownloadClient { get; set; } diff --git a/src/NzbDrone.Core/Notifications/INotification.cs b/src/NzbDrone.Core/Notifications/INotification.cs index 2d08d3316..770eaaf0c 100644 --- a/src/NzbDrone.Core/Notifications/INotification.cs +++ b/src/NzbDrone.Core/Notifications/INotification.cs @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Notifications void OnGrab(GrabMessage grabMessage); void OnReleaseImport(AlbumDownloadMessage message); - void OnRename(Artist artist); + void OnRename(Author artist); void OnHealthIssue(HealthCheck.HealthCheck healthCheck); void OnDownloadFailure(DownloadFailedMessage message); void OnImportFailure(AlbumDownloadMessage message); diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs deleted file mode 100644 index 8a5117578..000000000 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Collections.Generic; -using FluentValidation.Results; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Notifications.Emby -{ - public class MediaBrowser : NotificationBase - { - private readonly IMediaBrowserService _mediaBrowserService; - - public MediaBrowser(IMediaBrowserService mediaBrowserService) - { - _mediaBrowserService = mediaBrowserService; - } - - public override string Link => "https://emby.media/"; - public override string Name => "Emby (Media Browser)"; - - public override void OnGrab(GrabMessage grabMessage) - { - if (Settings.Notify) - { - _mediaBrowserService.Notify(Settings, ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message); - } - } - - public override void OnReleaseImport(AlbumDownloadMessage message) - { - if (Settings.Notify) - { - _mediaBrowserService.Notify(Settings, ALBUM_DOWNLOADED_TITLE_BRANDED, message.Message); - } - - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, message.Artist); - } - } - - public override void OnRename(Artist artist) - { - if (Settings.UpdateLibrary) - { - _mediaBrowserService.Update(Settings, artist); - } - } - - public override void OnHealthIssue(HealthCheck.HealthCheck message) - { - if (Settings.Notify) - { - _mediaBrowserService.Notify(Settings, HEALTH_ISSUE_TITLE_BRANDED, message.Message); - } - } - - public override void OnTrackRetag(TrackRetagMessage message) - { - if (Settings.Notify) - { - _mediaBrowserService.Notify(Settings, TRACK_RETAGGED_TITLE_BRANDED, message.Message); - } - } - - public override ValidationResult Test() - { - var failures = new List(); - - failures.AddIfNotNull(_mediaBrowserService.Test(Settings)); - - return new ValidationResult(failures); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs deleted file mode 100644 index 2f42ccab5..000000000 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Notifications.MediaBrowser.Model; - -namespace NzbDrone.Core.Notifications.Emby -{ - public class MediaBrowserProxy - { - private readonly IHttpClient _httpClient; - private readonly Logger _logger; - - public MediaBrowserProxy(IHttpClient httpClient, Logger logger) - { - _httpClient = httpClient; - _logger = logger; - } - - public void Notify(MediaBrowserSettings settings, string title, string message) - { - var path = "/Notifications/Admin"; - var request = BuildRequest(path, settings); - request.Headers.ContentType = "application/json"; - request.Method = HttpMethod.POST; - - request.SetContent(new - { - Name = title, - Description = message, - ImageUrl = "https://raw.github.com/readarr/Readarr/develop/Logo/64.png" - }.ToJson()); - - ProcessRequest(request, settings); - } - - public void Update(MediaBrowserSettings settings, List musicCollectionPaths) - { - string path; - HttpRequest request; - - if (musicCollectionPaths.Any()) - { - path = "/Library/Media/Updated"; - request = BuildRequest(path, settings); - request.Headers.ContentType = "application/json"; - - var updateInfo = new List(); - - foreach (var colPath in musicCollectionPaths) - { - updateInfo.Add(new EmbyMediaUpdateInfo - { - Path = colPath, - UpdateType = "Created" - }); - } - - request.SetContent(new - { - Updates = updateInfo - }.ToJson()); - } - else - { - path = "/Library/Refresh"; - request = BuildRequest(path, settings); - } - - request.Method = HttpMethod.POST; - - ProcessRequest(request, settings); - } - - private string ProcessRequest(HttpRequest request, MediaBrowserSettings settings) - { - request.Headers.Add("X-MediaBrowser-Token", settings.ApiKey); - - var response = _httpClient.Execute(request); - - _logger.Trace("Response: {0}", response.Content); - - CheckForError(response); - - return response.Content; - } - - private HttpRequest BuildRequest(string path, MediaBrowserSettings settings) - { - var scheme = settings.UseSsl ? "https" : "http"; - var url = $@"{scheme}://{settings.Address}/mediabrowser"; - - return new HttpRequestBuilder(url).Resource(path).Build(); - } - - private void CheckForError(HttpResponse response) - { - _logger.Debug("Looking for error in response: {0}", response); - - //TODO: actually check for the error - } - - public List GetArtist(MediaBrowserSettings settings) - { - var path = "/Library/MediaFolders"; - var request = BuildRequest(path, settings); - request.Method = HttpMethod.GET; - - var response = ProcessRequest(request, settings); - - return Json.Deserialize(response).Items; - } - } -} diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs deleted file mode 100644 index 0462441c1..000000000 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using FluentValidation.Results; -using NLog; -using NzbDrone.Core.Music; -using NzbDrone.Core.Rest; - -namespace NzbDrone.Core.Notifications.Emby -{ - public interface IMediaBrowserService - { - void Notify(MediaBrowserSettings settings, string title, string message); - void Update(MediaBrowserSettings settings, Artist artist); - ValidationFailure Test(MediaBrowserSettings settings); - } - - public class MediaBrowserService : IMediaBrowserService - { - private readonly MediaBrowserProxy _proxy; - private readonly Logger _logger; - - public MediaBrowserService(MediaBrowserProxy proxy, Logger logger) - { - _proxy = proxy; - _logger = logger; - } - - public void Notify(MediaBrowserSettings settings, string title, string message) - { - _proxy.Notify(settings, title, message); - } - - public void Update(MediaBrowserSettings settings, Artist artist) - { - var folders = _proxy.GetArtist(settings); - - var musicPaths = folders.Select(e => e.CollectionType = "music").ToList(); - - _proxy.Update(settings, musicPaths); - } - - public ValidationFailure Test(MediaBrowserSettings settings) - { - try - { - _logger.Debug("Testing connection to MediaBrowser: {0}", settings.Address); - - Notify(settings, "Test from Readarr", "Success! MediaBrowser has been successfully configured!"); - } - catch (RestException ex) - { - if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) - { - return new ValidationFailure("ApiKey", "API Key is incorrect"); - } - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to send test message"); - return new ValidationFailure("Host", "Unable to send test message: " + ex.Message); - } - - return null; - } - } -} diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs deleted file mode 100644 index 5669c77b6..000000000 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs +++ /dev/null @@ -1,55 +0,0 @@ -using FluentValidation; -using Newtonsoft.Json; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Notifications.Emby -{ - public class MediaBrowserSettingsValidator : AbstractValidator - { - public MediaBrowserSettingsValidator() - { - RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.ApiKey).NotEmpty(); - } - } - - public class MediaBrowserSettings : IProviderConfig - { - private static readonly MediaBrowserSettingsValidator Validator = new MediaBrowserSettingsValidator(); - - public MediaBrowserSettings() - { - Port = 8096; - } - - [FieldDefinition(0, Label = "Host")] - public string Host { get; set; } - - [FieldDefinition(1, Label = "Port")] - public int Port { get; set; } - - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Connect to Emby over HTTPS instead of HTTP")] - public bool UseSsl { get; set; } - - [FieldDefinition(3, Label = "API Key")] - public string ApiKey { get; set; } - - [FieldDefinition(4, Label = "Send Notifications", HelpText = "Have MediaBrowser send notfications to configured providers", Type = FieldType.Checkbox)] - public bool Notify { get; set; } - - [FieldDefinition(5, Label = "Update Library", HelpText = "Update Library on Download & Rename?", Type = FieldType.Checkbox)] - public bool UpdateLibrary { get; set; } - - [JsonIgnore] - public string Address => $"{Host}:{Port}"; - - public bool IsValid => !string.IsNullOrWhiteSpace(Host) && Port > 0; - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaFolder.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaFolder.cs deleted file mode 100644 index 68b421ead..000000000 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaFolder.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Notifications.MediaBrowser.Model -{ - public class EmbyMediaFolder - { - public string Path { get; set; } - public string CollectionType { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaFoldersResponse.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaFoldersResponse.cs deleted file mode 100644 index dd693001c..000000000 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaFoldersResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Notifications.MediaBrowser.Model -{ - public class EmbyMediaFoldersResponse - { - public List Items { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaUpdateInfo.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaUpdateInfo.cs deleted file mode 100644 index 292ce81a7..000000000 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/Model/EmbyMediaUpdateInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Notifications.MediaBrowser.Model -{ - public class EmbyMediaUpdateInfo - { - public string Path { get; set; } - public string UpdateType { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/NotificationBase.cs b/src/NzbDrone.Core/Notifications/NotificationBase.cs index 44eb85104..fe4764f1e 100644 --- a/src/NzbDrone.Core/Notifications/NotificationBase.cs +++ b/src/NzbDrone.Core/Notifications/NotificationBase.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Notifications { } - public virtual void OnRename(Artist artist) + public virtual void OnRename(Author artist) { } diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 87446dca6..6f486cbe1 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Notifications _logger = logger; } - private string GetMessage(Artist artist, List albums, QualityModel quality) + private string GetMessage(Author artist, List albums, QualityModel quality) { var qualityString = quality.Quality.ToString(); @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Notifications qualityString); } - private string GetAlbumDownloadMessage(Artist artist, Album album, List tracks) + private string GetAlbumDownloadMessage(Author artist, Book album, List tracks) { return string.Format("{0} - {1} ({2} Tracks Imported)", artist.Name, @@ -69,14 +69,14 @@ namespace NzbDrone.Core.Notifications return text.IsNullOrWhiteSpace() ? "" : text; } - private string GetTrackRetagMessage(Artist artist, TrackFile trackFile, Dictionary> diff) + private string GetTrackRetagMessage(Author artist, BookFile trackFile, Dictionary> diff) { return string.Format("{0}:\n{1}", trackFile.Path, string.Join("\n", diff.Select(x => $"{x.Key}: {FormatMissing(x.Value.Item1)} → {FormatMissing(x.Value.Item2)}"))); } - private bool ShouldHandleArtist(ProviderDefinition definition, Artist artist) + private bool ShouldHandleArtist(ProviderDefinition definition, Author artist) { if (definition.Tags.Empty()) { @@ -152,7 +152,6 @@ namespace NzbDrone.Core.Notifications Message = GetAlbumDownloadMessage(message.Artist, message.Album, message.ImportedTracks), Artist = message.Artist, Album = message.Album, - Release = message.AlbumRelease, DownloadClient = message.DownloadClient, DownloadId = message.DownloadId, TrackFiles = message.ImportedTracks, @@ -258,7 +257,6 @@ namespace NzbDrone.Core.Notifications Message = GetTrackRetagMessage(message.Artist, message.TrackFile, message.Diff), Artist = message.Artist, Album = message.TrackFile.Album, - Release = message.TrackFile.Tracks.Value.First().AlbumRelease.Value, TrackFile = message.TrackFile, Diff = message.Diff, Scrubbed = message.Scrubbed diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexAuthenticationException.cs b/src/NzbDrone.Core/Notifications/Plex/PlexAuthenticationException.cs deleted file mode 100644 index 4235168b4..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexAuthenticationException.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace NzbDrone.Core.Notifications.Plex -{ - public class PlexAuthenticationException : PlexException - { - public PlexAuthenticationException(string message) - : base(message) - { - } - - public PlexAuthenticationException(string message, params object[] args) - : base(message, args) - { - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexException.cs b/src/NzbDrone.Core/Notifications/Plex/PlexException.cs deleted file mode 100644 index dae85f6d9..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexException.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using NzbDrone.Common.Exceptions; - -namespace NzbDrone.Core.Notifications.Plex -{ - public class PlexException : NzbDroneException - { - public PlexException(string message) - : base(message) - { - } - - public PlexException(string message, params object[] args) - : base(message, args) - { - } - - public PlexException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinResponse.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinResponse.cs deleted file mode 100644 index aa46edb48..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.Notifications.Plex.PlexTv -{ - public class PlexTvPinResponse - { - public int Id { get; set; } - public string Code { get; set; } - public string AuthToken { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinUrlResponse.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinUrlResponse.cs deleted file mode 100644 index 4dace5645..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinUrlResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Notifications.Plex.PlexTv -{ - public class PlexTvPinUrlResponse - { - public string Url { get; set; } - public string Method => "POST"; - public Dictionary Headers { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs deleted file mode 100644 index c4fafe006..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Net; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Exceptions; - -namespace NzbDrone.Core.Notifications.Plex.PlexTv -{ - public interface IPlexTvProxy - { - string GetAuthToken(string clientIdentifier, int pinId); - } - - public class PlexTvProxy : IPlexTvProxy - { - private readonly IHttpClient _httpClient; - private readonly Logger _logger; - - public PlexTvProxy(IHttpClient httpClient, Logger logger) - { - _httpClient = httpClient; - _logger = logger; - } - - public string GetAuthToken(string clientIdentifier, int pinId) - { - var request = BuildRequest(clientIdentifier); - request.ResourceUrl = $"/api/v2/pins/{pinId}"; - - PlexTvPinResponse response; - - if (!Json.TryDeserialize(ProcessRequest(request), out response)) - { - response = new PlexTvPinResponse(); - } - - return response.AuthToken; - } - - private HttpRequestBuilder BuildRequest(string clientIdentifier) - { - var requestBuilder = new HttpRequestBuilder("https://plex.tv") - .Accept(HttpAccept.Json) - .AddQueryParam("X-Plex-Client-Identifier", clientIdentifier) - .AddQueryParam("X-Plex-Product", BuildInfo.AppName) - .AddQueryParam("X-Plex-Platform", "Windows") - .AddQueryParam("X-Plex-Platform-Version", "7") - .AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName) - .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()); - - return requestBuilder; - } - - private string ProcessRequest(HttpRequestBuilder requestBuilder) - { - var httpRequest = requestBuilder.Build(); - - HttpResponse response; - - _logger.Debug("Url: {0}", httpRequest.Url); - - try - { - response = _httpClient.Execute(httpRequest); - } - catch (HttpException ex) - { - throw new NzbDroneClientException(ex.Response.StatusCode, "Unable to connect to plex.tv"); - } - catch (WebException) - { - throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv"); - } - - return response.Content; - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs deleted file mode 100644 index 533f5d866..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Linq; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Core.Notifications.Plex.PlexTv -{ - public interface IPlexTvService - { - PlexTvPinUrlResponse GetPinUrl(); - PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode); - string GetAuthToken(int pinId); - } - - public class PlexTvService : IPlexTvService - { - private readonly IPlexTvProxy _proxy; - private readonly IConfigService _configService; - - public PlexTvService(IPlexTvProxy proxy, IConfigService configService) - { - _proxy = proxy; - _configService = configService; - } - - public PlexTvPinUrlResponse GetPinUrl() - { - var clientIdentifier = _configService.PlexClientIdentifier; - - var requestBuilder = new HttpRequestBuilder("https://plex.tv/api/v2/pins") - .Accept(HttpAccept.Json) - .AddQueryParam("X-Plex-Client-Identifier", clientIdentifier) - .AddQueryParam("X-Plex-Product", BuildInfo.AppName) - .AddQueryParam("X-Plex-Platform", "Windows") - .AddQueryParam("X-Plex-Platform-Version", "7") - .AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName) - .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()) - .AddQueryParam("strong", true); - - requestBuilder.Method = HttpMethod.POST; - - var request = requestBuilder.Build(); - - return new PlexTvPinUrlResponse - { - Url = request.Url.ToString(), - Headers = request.Headers.ToDictionary(h => h.Key, h => h.Value) - }; - } - - public PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode) - { - var clientIdentifier = _configService.PlexClientIdentifier; - - var requestBuilder = new HttpRequestBuilder("https://app.plex.tv/auth/hashBang") - .AddQueryParam("clientID", clientIdentifier) - .AddQueryParam("forwardUrl", callbackUrl) - .AddQueryParam("code", pinCode) - .AddQueryParam("context[device][product]", BuildInfo.AppName) - .AddQueryParam("context[device][platform]", "Windows") - .AddQueryParam("context[device][platformVersion]", "7") - .AddQueryParam("context[device][version]", BuildInfo.Version.ToString()); - - // #! is stripped out of the URL when building, this works around it. - requestBuilder.Segments.Add("hashBang", "#!"); - - var request = requestBuilder.Build(); - - return new PlexTvSignInUrlResponse - { - OauthUrl = request.Url.ToString(), - PinId = pinId - }; - } - - public string GetAuthToken(int pinId) - { - var authToken = _proxy.GetAuthToken(_configService.PlexClientIdentifier, pinId); - - return authToken; - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvSignInUrlResponse.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvSignInUrlResponse.cs deleted file mode 100644 index 33bd2a8ff..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvSignInUrlResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Notifications.Plex.PlexTv -{ - public class PlexTvSignInUrlResponse - { - public string OauthUrl { get; set; } - public int PinId { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexVersionException.cs b/src/NzbDrone.Core/Notifications/Plex/PlexVersionException.cs deleted file mode 100644 index 42c7889da..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexVersionException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using NzbDrone.Common.Exceptions; - -namespace NzbDrone.Core.Notifications.Plex -{ - public class PlexVersionException : NzbDroneException - { - public PlexVersionException(string message) - : base(message) - { - } - - public PlexVersionException(string message, params object[] args) - : base(message, args) - { - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexError.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexError.cs deleted file mode 100644 index 3018c080a..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexError.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public class PlexError - { - public string Error { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexIdentity.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexIdentity.cs deleted file mode 100644 index 9762421e8..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexIdentity.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public class PlexIdentity - { - public string MachineIdentifier { get; set; } - public string Version { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexPreferences.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexPreferences.cs deleted file mode 100644 index dc1ebc3a1..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexPreferences.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public class PlexPreferences - { - [JsonProperty("Setting")] - public List Preferences { get; set; } - } - - public class PlexPreference - { - public string Id { get; set; } - public string Type { get; set; } - public string Value { get; set; } - } - - public class PlexPreferencesLegacy - { - [JsonProperty("_children")] - public List Preferences { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexResponse.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexResponse.cs deleted file mode 100644 index af57abc50..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexResponse.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public class PlexResponse - { - public T MediaContainer { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexSection.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexSection.cs deleted file mode 100644 index f366b246b..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexSection.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public class PlexSectionLocation - { - public int Id { get; set; } - public string Path { get; set; } - } - - public class PlexSection - { - public PlexSection() - { - Locations = new List(); - } - - [JsonProperty("key")] - public int Id { get; set; } - - public string Type { get; set; } - public string Language { get; set; } - - [JsonProperty("Location")] - public List Locations { get; set; } - } - - public class PlexSectionsContainer - { - public PlexSectionsContainer() - { - Sections = new List(); - } - - [JsonProperty("Directory")] - public List Sections { get; set; } - } - - public class PlexSectionLegacy - { - [JsonProperty("key")] - public int Id { get; set; } - - public string Type { get; set; } - public string Language { get; set; } - - [JsonProperty("_children")] - public List Locations { get; set; } - } - - public class PlexMediaContainerLegacy - { - [JsonProperty("_children")] - public List Sections { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexSectionItem.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexSectionItem.cs deleted file mode 100644 index dfd381465..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexSectionItem.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public class PlexSectionItem - { - [JsonProperty("ratingKey")] - public int Id { get; set; } - - public string Title { get; set; } - } - - public class PlexSectionResponse - { - [JsonProperty("Metadata")] - public List Items { get; set; } - } - - public class PlexSectionResponseLegacy - { - [JsonProperty("_children")] - public List Items { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs deleted file mode 100644 index 565b7c99c..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using FluentValidation.Results; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Exceptions; -using NzbDrone.Core.Music; -using NzbDrone.Core.Notifications.Plex.PlexTv; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public class PlexServer : NotificationBase - { - private readonly IPlexServerService _plexServerService; - private readonly IPlexTvService _plexTvService; - - public PlexServer(IPlexServerService plexServerService, IPlexTvService plexTvService) - { - _plexServerService = plexServerService; - _plexTvService = plexTvService; - } - - public override string Link => "https://www.plex.tv/"; - public override string Name => "Plex Media Server"; - - public override void OnReleaseImport(AlbumDownloadMessage message) - { - UpdateIfEnabled(message.Artist); - } - - public override void OnRename(Artist artist) - { - UpdateIfEnabled(artist); - } - - public override void OnTrackRetag(TrackRetagMessage message) - { - UpdateIfEnabled(message.Artist); - } - - private void UpdateIfEnabled(Artist artist) - { - if (Settings.UpdateLibrary) - { - _plexServerService.UpdateLibrary(artist, Settings); - } - } - - public override ValidationResult Test() - { - var failures = new List(); - - failures.AddIfNotNull(_plexServerService.Test(Settings)); - - return new ValidationResult(failures); - } - - public override object RequestAction(string action, IDictionary query) - { - if (action == "startOAuth") - { - Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError(); - - return _plexTvService.GetPinUrl(); - } - else if (action == "continueOAuth") - { - Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError(); - - if (query["callbackUrl"].IsNullOrWhiteSpace()) - { - throw new BadRequestException("QueryParam callbackUrl invalid."); - } - - if (query["id"].IsNullOrWhiteSpace()) - { - throw new BadRequestException("QueryParam id invalid."); - } - - if (query["code"].IsNullOrWhiteSpace()) - { - throw new BadRequestException("QueryParam code invalid."); - } - - return _plexTvService.GetSignInUrl(query["callbackUrl"], Convert.ToInt32(query["id"]), query["code"]); - } - else if (action == "getOAuthToken") - { - Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError(); - - if (query["pinId"].IsNullOrWhiteSpace()) - { - throw new BadRequestException("QueryParam pinId invalid."); - } - - var authToken = _plexTvService.GetAuthToken(Convert.ToInt32(query["pinId"])); - - return new - { - authToken - }; - } - - return new { }; - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs deleted file mode 100644 index 9ae241580..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public interface IPlexServerProxy - { - List GetArtistSections(PlexServerSettings settings); - void Update(int sectionId, PlexServerSettings settings); - void UpdateArtist(int metadataId, PlexServerSettings settings); - string Version(PlexServerSettings settings); - List Preferences(PlexServerSettings settings); - int? GetMetadataId(int sectionId, string mbId, string language, PlexServerSettings settings); - } - - public class PlexServerProxy : IPlexServerProxy - { - private readonly IHttpClient _httpClient; - private readonly IConfigService _configService; - private readonly Logger _logger; - - public PlexServerProxy(IHttpClient httpClient, IConfigService configService, Logger logger) - { - _httpClient = httpClient; - _configService = configService; - _logger = logger; - } - - public List GetArtistSections(PlexServerSettings settings) - { - var request = BuildRequest("library/sections", HttpMethod.GET, settings); - var response = ProcessRequest(request); - - CheckForError(response); - - if (response.Contains("_children")) - { - return Json.Deserialize(response) - .Sections - .Where(d => d.Type == "artist") - .Select(s => new PlexSection - { - Id = s.Id, - Language = s.Language, - Locations = s.Locations, - Type = s.Type - }) - .ToList(); - } - - return Json.Deserialize>(response) - .MediaContainer - .Sections - .Where(d => d.Type == "artist") - .ToList(); - } - - public void Update(int sectionId, PlexServerSettings settings) - { - var resource = $"library/sections/{sectionId}/refresh"; - var request = BuildRequest(resource, HttpMethod.GET, settings); - var response = ProcessRequest(request); - - CheckForError(response); - } - - public void UpdateArtist(int metadataId, PlexServerSettings settings) - { - var resource = $"library/metadata/{metadataId}/refresh"; - var request = BuildRequest(resource, HttpMethod.PUT, settings); - var response = ProcessRequest(request); - - CheckForError(response); - } - - public string Version(PlexServerSettings settings) - { - var request = BuildRequest("identity", HttpMethod.GET, settings); - var response = ProcessRequest(request); - - CheckForError(response); - - if (response.Contains("_children")) - { - return Json.Deserialize(response) - .Version; - } - - return Json.Deserialize>(response) - .MediaContainer - .Version; - } - - public List Preferences(PlexServerSettings settings) - { - var request = BuildRequest(":/prefs", HttpMethod.GET, settings); - var response = ProcessRequest(request); - - CheckForError(response); - - if (response.Contains("_children")) - { - return Json.Deserialize(response) - .Preferences; - } - - return Json.Deserialize>(response) - .MediaContainer - .Preferences; - } - - public int? GetMetadataId(int sectionId, string mbId, string language, PlexServerSettings settings) - { - var guid = string.Format("com.plexapp.agents.lastfm://{0}?lang={1}", mbId, language); // TODO Plex Route for MB? LastFM? - var resource = $"library/sections/{sectionId}/all?guid={System.Web.HttpUtility.UrlEncode(guid)}"; - var request = BuildRequest(resource, HttpMethod.GET, settings); - var response = ProcessRequest(request); - - CheckForError(response); - - List items; - - if (response.Contains("_children")) - { - items = Json.Deserialize(response) - .Items; - } - else - { - items = Json.Deserialize>(response) - .MediaContainer - .Items; - } - - if (items == null || items.Empty()) - { - return null; - } - - return items.First().Id; - } - - private HttpRequestBuilder BuildRequest(string resource, HttpMethod method, PlexServerSettings settings) - { - var scheme = settings.UseSsl ? "https" : "http"; - var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host}:{settings.Port}") - .Accept(HttpAccept.Json) - .AddQueryParam("X-Plex-Client-Identifier", _configService.PlexClientIdentifier) - .AddQueryParam("X-Plex-Product", BuildInfo.AppName) - .AddQueryParam("X-Plex-Platform", "Windows") - .AddQueryParam("X-Plex-Platform-Version", "7") - .AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName) - .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()); - - if (settings.AuthToken.IsNotNullOrWhiteSpace()) - { - requestBuilder.AddQueryParam("X-Plex-Token", settings.AuthToken); - } - - requestBuilder.ResourceUrl = resource; - requestBuilder.Method = method; - - return requestBuilder; - } - - private string ProcessRequest(HttpRequestBuilder requestBuilder) - { - var httpRequest = requestBuilder.Build(); - - HttpResponse response; - - _logger.Debug("Url: {0}", httpRequest.Url); - - try - { - response = _httpClient.Execute(httpRequest); - } - catch (HttpException ex) - { - if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) - { - throw new PlexAuthenticationException("Unauthorized - AuthToken is invalid"); - } - - throw new PlexException("Unable to connect to Plex Media Server. Status Code: {0}", ex.Response.StatusCode); - } - catch (WebException ex) - { - throw new PlexException("Unable to connect to Plex Media Server", ex); - } - - return response.Content; - } - - private void CheckForError(string response) - { - _logger.Trace("Checking for error"); - - if (response.IsNullOrWhiteSpace()) - { - _logger.Trace("No response body returned, no error detected"); - return; - } - - var error = response.Contains("_children") ? - Json.Deserialize(response) : - Json.Deserialize>(response).MediaContainer; - - if (error != null && !error.Error.IsNullOrWhiteSpace()) - { - throw new PlexException(error.Error); - } - - _logger.Trace("No error detected"); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs deleted file mode 100644 index 5c394bf62..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using FluentValidation.Results; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public interface IPlexServerService - { - void UpdateLibrary(Artist artist, PlexServerSettings settings); - ValidationFailure Test(PlexServerSettings settings); - } - - public class PlexServerService : IPlexServerService - { - private readonly ICached _versionCache; - private readonly ICached _partialUpdateCache; - private readonly IPlexServerProxy _plexServerProxy; - private readonly Logger _logger; - - public PlexServerService(ICacheManager cacheManager, IPlexServerProxy plexServerProxy, Logger logger) - { - _versionCache = cacheManager.GetCache(GetType(), "versionCache"); - _partialUpdateCache = cacheManager.GetCache(GetType(), "partialUpdateCache"); - _plexServerProxy = plexServerProxy; - _logger = logger; - } - - public void UpdateLibrary(Artist artist, PlexServerSettings settings) - { - try - { - _logger.Debug("Sending Update Request to Plex Server"); - - var version = _versionCache.Get(settings.Host, () => GetVersion(settings), TimeSpan.FromHours(2)); - ValidateVersion(version); - - var sections = GetSections(settings); - var partialUpdates = _partialUpdateCache.Get(settings.Host, () => PartialUpdatesAllowed(settings, version), TimeSpan.FromHours(2)); - - if (partialUpdates) - { - UpdatePartialSection(artist, sections, settings); - } - else - { - sections.ForEach(s => UpdateSection(s.Id, settings)); - } - } - catch (Exception ex) - { - _logger.Warn(ex, "Failed to Update Plex host: " + settings.Host); - throw; - } - } - - private List GetSections(PlexServerSettings settings) - { - _logger.Debug("Getting sections from Plex host: {0}", settings.Host); - - return _plexServerProxy.GetArtistSections(settings).ToList(); - } - - private bool PartialUpdatesAllowed(PlexServerSettings settings, Version version) - { - try - { - if (version >= new Version(0, 9, 12, 0)) - { - var preferences = GetPreferences(settings); - var partialScanPreference = preferences.SingleOrDefault(p => p.Id.Equals("FSEventLibraryPartialScanEnabled")); - - if (partialScanPreference == null) - { - return false; - } - - return Convert.ToBoolean(partialScanPreference.Value); - } - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to check if partial updates are allowed"); - } - - return false; - } - - private void ValidateVersion(Version version) - { - if (version >= new Version(1, 3, 0) && version < new Version(1, 3, 1)) - { - throw new PlexVersionException("Found version {0}, upgrade to PMS 1.3.1 to fix library updating and then restart Readarr", version); - } - } - - private Version GetVersion(PlexServerSettings settings) - { - _logger.Debug("Getting version from Plex host: {0}", settings.Host); - - var rawVersion = _plexServerProxy.Version(settings); - var version = new Version(Regex.Match(rawVersion, @"^(\d+[.-]){4}").Value.Trim('.', '-')); - - return version; - } - - private List GetPreferences(PlexServerSettings settings) - { - _logger.Debug("Getting preferences from Plex host: {0}", settings.Host); - - return _plexServerProxy.Preferences(settings); - } - - private void UpdateSection(int sectionId, PlexServerSettings settings) - { - _logger.Debug("Updating Plex host: {0}, Section: {1}", settings.Host, sectionId); - - _plexServerProxy.Update(sectionId, settings); - } - - private void UpdatePartialSection(Artist artist, List sections, PlexServerSettings settings) - { - var partiallyUpdated = false; - - foreach (var section in sections) - { - var metadataId = GetMetadataId(section.Id, artist, section.Language, settings); - - if (metadataId.HasValue) - { - _logger.Debug("Updating Plex host: {0}, Section: {1}, Artist: {2}", settings.Host, section.Id, artist); - _plexServerProxy.UpdateArtist(metadataId.Value, settings); - - partiallyUpdated = true; - } - } - - // Only update complete sections if all partial updates failed - if (!partiallyUpdated) - { - _logger.Debug("Unable to update partial section, updating all Music sections"); - sections.ForEach(s => UpdateSection(s.Id, settings)); - } - } - - private int? GetMetadataId(int sectionId, Artist artist, string language, PlexServerSettings settings) - { - _logger.Debug("Getting metadata from Plex host: {0} for artist: {1}", settings.Host, artist); - - return _plexServerProxy.GetMetadataId(sectionId, artist.Metadata.Value.ForeignArtistId, language, settings); - } - - public ValidationFailure Test(PlexServerSettings settings) - { - try - { - var sections = GetSections(settings); - - if (sections.Empty()) - { - return new ValidationFailure("Host", "At least one Music library is required"); - } - } - catch (PlexAuthenticationException ex) - { - _logger.Error(ex, "Unable to connect to Plex Server"); - return new ValidationFailure("AuthToken", "Invalid authentication token"); - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to connect to Plex Server"); - return new ValidationFailure("Host", "Unable to connect to Plex Server"); - } - - return null; - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs deleted file mode 100644 index af2241697..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs +++ /dev/null @@ -1,53 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Notifications.Plex.Server -{ - public class PlexServerSettingsValidator : AbstractValidator - { - public PlexServerSettingsValidator() - { - RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).InclusiveBetween(1, 65535); - } - } - - public class PlexServerSettings : IProviderConfig - { - private static readonly PlexServerSettingsValidator Validator = new PlexServerSettingsValidator(); - - public PlexServerSettings() - { - Port = 32400; - UpdateLibrary = true; - SignIn = "startOAuth"; - } - - [FieldDefinition(0, Label = "Host")] - public string Host { get; set; } - - [FieldDefinition(1, Label = "Port")] - public int Port { get; set; } - - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Connect to Plex over HTTPS instead of HTTP")] - public bool UseSsl { get; set; } - - [FieldDefinition(3, Label = "Auth Token", Type = FieldType.Textbox, Advanced = true)] - public string AuthToken { get; set; } - - [FieldDefinition(4, Label = "Authenticate with Plex.tv", Type = FieldType.OAuth)] - public string SignIn { get; set; } - - [FieldDefinition(5, Label = "Update Library", Type = FieldType.Checkbox)] - public bool UpdateLibrary { get; set; } - - public bool IsValid => !string.IsNullOrWhiteSpace(Host); - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index b082361f9..7495dfd52 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Notifications.Slack _proxy.SendPayload(payload, Settings); } - public override void OnRename(Artist artist) + public override void OnRename(Author artist) { var attachments = new List { diff --git a/src/NzbDrone.Core/Notifications/Subsonic/Subsonic.cs b/src/NzbDrone.Core/Notifications/Subsonic/Subsonic.cs index 4eb2fc8ea..89df9f923 100644 --- a/src/NzbDrone.Core/Notifications/Subsonic/Subsonic.cs +++ b/src/NzbDrone.Core/Notifications/Subsonic/Subsonic.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Notifications.Subsonic Update(); } - public override void OnRename(Artist artist) + public override void OnRename(Author artist) { Update(); } diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs index d4f8a6581..1fb649b73 100644 --- a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Notifications.Synology } } - public override void OnRename(Artist artist) + public override void OnRename(Author artist) { if (Settings.UpdateLibrary) { diff --git a/src/NzbDrone.Core/Notifications/TrackRetagMessage.cs b/src/NzbDrone.Core/Notifications/TrackRetagMessage.cs index 6f6702544..5292cd644 100644 --- a/src/NzbDrone.Core/Notifications/TrackRetagMessage.cs +++ b/src/NzbDrone.Core/Notifications/TrackRetagMessage.cs @@ -8,10 +8,9 @@ namespace NzbDrone.Core.Notifications public class TrackRetagMessage { public string Message { get; set; } - public Artist Artist { get; set; } - public Album Album { get; set; } - public AlbumRelease Release { get; set; } - public TrackFile TrackFile { get; set; } + public Author Artist { get; set; } + public Book Album { get; set; } + public BookFile TrackFile { get; set; } public Dictionary> Diff { get; set; } public bool Scrubbed { get; set; } diff --git a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs index a34798bd7..57992ecd3 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs @@ -48,13 +48,7 @@ namespace NzbDrone.Core.Notifications.Webhook { EventType = "Download", Artist = new WebhookArtist(message.Artist), - Tracks = trackFiles.SelectMany(x => x.Tracks.Value.Select(y => new WebhookTrack(y) - { - // TODO: Stop passing these parameters inside an episode v3 - Quality = x.Quality.Quality.Name, - QualityVersion = x.Quality.Revision.Version, - ReleaseGroup = x.ReleaseGroup - })).ToList(), + Book = new WebhookAlbum(message.Album), TrackFiles = trackFiles.ConvertAll(x => new WebhookTrackFile(x)), IsUpgrade = message.OldFiles.Any() }; @@ -62,7 +56,7 @@ namespace NzbDrone.Core.Notifications.Webhook _proxy.SendWebhook(payload, Settings); } - public override void OnRename(Artist artist) + public override void OnRename(Author artist) { var payload = new WebhookPayload { diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookAlbum.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookAlbum.cs index 5b82e4eef..b9f13a82c 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookAlbum.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookAlbum.cs @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Notifications.Webhook { } - public WebhookAlbum(Album album) + public WebhookAlbum(Book album) { Id = album.Id; Title = album.Title; diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookArtist.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookArtist.cs index 11e9199bd..8dd1899ab 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookArtist.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookArtist.cs @@ -13,12 +13,12 @@ namespace NzbDrone.Core.Notifications.Webhook { } - public WebhookArtist(Artist artist) + public WebhookArtist(Author artist) { Id = artist.Id; Name = artist.Name; Path = artist.Path; - MBId = artist.Metadata.Value.ForeignArtistId; + MBId = artist.Metadata.Value.ForeignAuthorId; } } } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookImportPayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookImportPayload.cs index ec19633d7..988f64f78 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookImportPayload.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookImportPayload.cs @@ -4,7 +4,7 @@ namespace NzbDrone.Core.Notifications.Webhook { public class WebhookImportPayload : WebhookPayload { - public List Tracks { get; set; } + public WebhookAlbum Book { get; set; } public List TrackFiles { get; set; } public bool IsUpgrade { get; set; } } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookTrack.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookTrack.cs deleted file mode 100644 index 9cef65c2a..000000000 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookTrack.cs +++ /dev/null @@ -1,26 +0,0 @@ -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Notifications.Webhook -{ - public class WebhookTrack - { - public WebhookTrack() - { - } - - public WebhookTrack(Track track) - { - Id = track.Id; - Title = track.Title; - TrackNumber = track.TrackNumber; - } - - public int Id { get; set; } - public string Title { get; set; } - public string TrackNumber { get; set; } - - public string Quality { get; set; } - public int QualityVersion { get; set; } - public string ReleaseGroup { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs index 60a6fe799..f5ee8d8bc 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Notifications.Webhook { } - public WebhookTrackFile(TrackFile trackFile) + public WebhookTrackFile(BookFile trackFile) { Id = trackFile.Id; Path = trackFile.Path; diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Model/ActivePlayer.cs b/src/NzbDrone.Core/Notifications/Xbmc/Model/ActivePlayer.cs deleted file mode 100644 index ad4dafb68..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/Model/ActivePlayer.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace NzbDrone.Core.Notifications.Xbmc.Model -{ - public class ActivePlayer - { - public int PlayerId { get; set; } - public string Type { get; set; } - - public ActivePlayer(int playerId, string type) - { - PlayerId = playerId; - Type = type; - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Model/ActivePlayersResult.cs b/src/NzbDrone.Core/Notifications/Xbmc/Model/ActivePlayersResult.cs deleted file mode 100644 index 6868ae48b..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/Model/ActivePlayersResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Notifications.Xbmc.Model -{ - public class ActivePlayersResult - { - public string Id { get; set; } - public string JsonRpc { get; set; } - public List Result { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Model/ArtistResponse.cs b/src/NzbDrone.Core/Notifications/Xbmc/Model/ArtistResponse.cs deleted file mode 100644 index bf1911901..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/Model/ArtistResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.Notifications.Xbmc.Model -{ - public class ArtistResponse - { - public string Id { get; set; } - public string JsonRpc { get; set; } - public ArtistResult Result { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Model/ArtistResult.cs b/src/NzbDrone.Core/Notifications/Xbmc/Model/ArtistResult.cs deleted file mode 100644 index 9acb0ad11..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/Model/ArtistResult.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Notifications.Xbmc.Model -{ - public class ArtistResult - { - public Dictionary Limits { get; set; } - public List Artists; - - public ArtistResult() - { - Artists = new List(); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Model/ErrorResult.cs b/src/NzbDrone.Core/Notifications/Xbmc/Model/ErrorResult.cs deleted file mode 100644 index 8de9b7c58..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/Model/ErrorResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Notifications.Xbmc.Model -{ - public class ErrorResult - { - public string Id { get; set; } - public string JsonRpc { get; set; } - public Dictionary Error { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Model/KodiArtist.cs b/src/NzbDrone.Core/Notifications/Xbmc/Model/KodiArtist.cs deleted file mode 100644 index 1e86c7c9d..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/Model/KodiArtist.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Notifications.Xbmc.Model -{ - public class KodiArtist - { - public int ArtistId { get; set; } - public string Label { get; set; } - public List MusicbrainzArtistId { get; set; } - public string File { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Model/XbmcJsonResult.cs b/src/NzbDrone.Core/Notifications/Xbmc/Model/XbmcJsonResult.cs deleted file mode 100644 index d69dc4902..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/Model/XbmcJsonResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.Notifications.Xbmc.Model -{ - public class XbmcJsonResult - { - public string Id { get; set; } - public string JsonRpc { get; set; } - public T Result { get; set; } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs deleted file mode 100644 index 09077288b..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; -using FluentValidation.Results; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Notifications.Xbmc -{ - public class Xbmc : NotificationBase - { - private readonly IXbmcService _xbmcService; - private readonly Logger _logger; - - public Xbmc(IXbmcService xbmcService, Logger logger) - { - _xbmcService = xbmcService; - _logger = logger; - } - - public override string Link => "http://xbmc.org/"; - - public override void OnGrab(GrabMessage grabMessage) - { - const string header = "Readarr - Grabbed"; - - Notify(Settings, header, grabMessage.Message); - } - - public override void OnReleaseImport(AlbumDownloadMessage message) - { - const string header = "Readarr - Downloaded"; - - Notify(Settings, header, message.Message); - UpdateAndClean(message.Artist, message.OldFiles.Any()); - } - - public override void OnRename(Artist artist) - { - UpdateAndClean(artist); - } - - public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) - { - Notify(Settings, HEALTH_ISSUE_TITLE_BRANDED, healthCheck.Message); - } - - public override void OnTrackRetag(TrackRetagMessage message) - { - UpdateAndClean(message.Artist); - } - - public override string Name => "Kodi"; - - public override ValidationResult Test() - { - var failures = new List(); - - failures.AddIfNotNull(_xbmcService.Test(Settings, "Success! Kodi has been successfully configured!")); - - return new ValidationResult(failures); - } - - private void Notify(XbmcSettings settings, string header, string message) - { - try - { - if (Settings.Notify) - { - _xbmcService.Notify(Settings, header, message); - } - } - catch (SocketException ex) - { - var logMessage = string.Format("Unable to connect to Kodi Host: {0}:{1}", Settings.Host, Settings.Port); - _logger.Debug(ex, logMessage); - } - } - - private void UpdateAndClean(Artist artist, bool clean = true) - { - try - { - if (Settings.UpdateLibrary) - { - _xbmcService.Update(Settings, artist); - } - - if (clean && Settings.CleanLibrary) - { - _xbmcService.Clean(Settings); - } - } - catch (SocketException ex) - { - var logMessage = string.Format("Unable to connect to Kodi Host: {0}:{1}", Settings.Host, Settings.Port); - _logger.Debug(ex, logMessage); - } - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs deleted file mode 100644 index eeaf75385..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Collections.Generic; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Notifications.Xbmc.Model; -using NzbDrone.Core.Rest; -using RestSharp; -using RestSharp.Authenticators; - -namespace NzbDrone.Core.Notifications.Xbmc -{ - public interface IXbmcJsonApiProxy - { - string GetJsonVersion(XbmcSettings settings); - void Notify(XbmcSettings settings, string title, string message); - string UpdateLibrary(XbmcSettings settings, string path); - void CleanLibrary(XbmcSettings settings); - List GetActivePlayers(XbmcSettings settings); - List GetArtist(XbmcSettings settings); - } - - public class XbmcJsonApiProxy : IXbmcJsonApiProxy - { - private readonly Logger _logger; - - public XbmcJsonApiProxy(Logger logger) - { - _logger = logger; - } - - public string GetJsonVersion(XbmcSettings settings) - { - var request = new RestRequest(); - return ProcessRequest(request, settings, "JSONRPC.Version"); - } - - public void Notify(XbmcSettings settings, string title, string message) - { - var request = new RestRequest(); - - var parameters = new Dictionary(); - parameters.Add("title", title); - parameters.Add("message", message); - parameters.Add("image", "https://raw.github.com/Readarr/Readarr/develop/Logo/64.png"); - parameters.Add("displaytime", settings.DisplayTime * 1000); - - ProcessRequest(request, settings, "GUI.ShowNotification", parameters); - } - - public string UpdateLibrary(XbmcSettings settings, string path) - { - var request = new RestRequest(); - var parameters = new Dictionary(); - parameters.Add("directory", path); - - if (path.IsNullOrWhiteSpace()) - { - parameters = null; - } - - var response = ProcessRequest(request, settings, "AudioLibrary.Scan", parameters); - - return Json.Deserialize>(response).Result; - } - - public void CleanLibrary(XbmcSettings settings) - { - var request = new RestRequest(); - - ProcessRequest(request, settings, "AudioLibrary.Clean"); - } - - public List GetActivePlayers(XbmcSettings settings) - { - var request = new RestRequest(); - - var response = ProcessRequest(request, settings, "Player.GetActivePlayers"); - - return Json.Deserialize(response).Result; - } - - public List GetArtist(XbmcSettings settings) - { - var request = new RestRequest(); - var parameters = new Dictionary(); - parameters.Add("properties", new[] { "musicbrainzartistid" }); //TODO: Figure out why AudioLibrary doesnt list file location like videoLibray - - var response = ProcessRequest(request, settings, "AudioLibrary.GetArtists", parameters); - - return Json.Deserialize(response).Result.Artists; - } - - private string ProcessRequest(IRestRequest request, XbmcSettings settings, string method, Dictionary parameters = null) - { - var client = BuildClient(settings); - - request.Method = Method.POST; - request.RequestFormat = DataFormat.Json; - request.JsonSerializer = new JsonNetSerializer(); - request.AddBody(new { jsonrpc = "2.0", method = method, id = 10, @params = parameters }); - - var response = client.ExecuteAndValidate(request); - _logger.Trace("Response: {0}", response.Content); - - CheckForError(response); - - return response.Content; - } - - private IRestClient BuildClient(XbmcSettings settings) - { - var url = string.Format(@"http://{0}/jsonrpc", settings.Address); - var client = RestClientFactory.BuildClient(url); - - if (!settings.Username.IsNullOrWhiteSpace()) - { - client.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password); - } - - return client; - } - - private void CheckForError(IRestResponse response) - { - if (string.IsNullOrWhiteSpace(response.Content)) - { - throw new XbmcJsonException("Invalid response from XBMC, the response is not valid JSON"); - } - - _logger.Trace("Looking for error in response, {0}", response.Content); - - if (response.Content.StartsWith("{\"error\"")) - { - var error = Json.Deserialize(response.Content); - var code = error.Error["code"]; - var message = error.Error["message"]; - - var errorMessage = string.Format("XBMC Json Error. Code = {0}, Message: {1}", code, message); - throw new XbmcJsonException(errorMessage); - } - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonException.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonException.cs deleted file mode 100644 index 95e5bbcf4..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace NzbDrone.Core.Notifications.Xbmc -{ - public class XbmcJsonException : Exception - { - public XbmcJsonException() - { - } - - public XbmcJsonException(string message) - : base(message) - { - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs deleted file mode 100644 index 334837b95..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Linq; -using FluentValidation.Results; -using NLog; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Notifications.Xbmc -{ - public interface IXbmcService - { - void Notify(XbmcSettings settings, string title, string message); - void Update(XbmcSettings settings, Artist artist); - void Clean(XbmcSettings settings); - ValidationFailure Test(XbmcSettings settings, string message); - } - - public class XbmcService : IXbmcService - { - private readonly IXbmcJsonApiProxy _proxy; - private readonly Logger _logger; - - public XbmcService(IXbmcJsonApiProxy proxy, - Logger logger) - { - _proxy = proxy; - _logger = logger; - } - - public void Notify(XbmcSettings settings, string title, string message) - { - _proxy.Notify(settings, title, message); - } - - public void Update(XbmcSettings settings, Artist artist) - { - if (!settings.AlwaysUpdate) - { - _logger.Debug("Determining if there are any active players on XBMC host: {0}", settings.Address); - var activePlayers = _proxy.GetActivePlayers(settings); - - if (activePlayers.Any(a => a.Type.Equals("audio"))) - { - _logger.Debug("Audio is currently playing, skipping library update"); - return; - } - } - - UpdateLibrary(settings, artist); - } - - public void Clean(XbmcSettings settings) - { - _proxy.CleanLibrary(settings); - } - - public string GetArtistPath(XbmcSettings settings, Artist artist) - { - var allArtists = _proxy.GetArtist(settings); - - if (!allArtists.Any()) - { - _logger.Debug("No Artists returned from XBMC"); - return null; - } - - var matchingArtist = allArtists.FirstOrDefault(s => - { - var musicBrainzId = s.MusicbrainzArtistId.FirstOrDefault(); - - return musicBrainzId == artist.Metadata.Value.ForeignArtistId || s.Label == artist.Name; - }); - - return matchingArtist?.File; - } - - private void UpdateLibrary(XbmcSettings settings, Artist artist) - { - try - { - var artistPath = GetArtistPath(settings, artist); - - if (artistPath != null) - { - _logger.Debug("Updating artist {0} (Path: {1}) on XBMC host: {2}", artist, artistPath, settings.Address); - } - else - { - _logger.Debug("Artist {0} doesn't exist on XBMC host: {1}, Updating Entire Library", - artist, - settings.Address); - } - - var response = _proxy.UpdateLibrary(settings, artistPath); - - if (!response.Equals("OK", StringComparison.InvariantCultureIgnoreCase)) - { - _logger.Debug("Failed to update library for: {0}", settings.Address); - } - } - catch (Exception ex) - { - _logger.Debug(ex, ex.Message); - } - } - - public ValidationFailure Test(XbmcSettings settings, string message) - { - try - { - Notify(settings, "Test Notification", message); - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to send test message"); - return new ValidationFailure("Host", "Unable to send test message"); - } - - return null; - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs deleted file mode 100644 index 21293e843..000000000 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.ComponentModel; -using FluentValidation; -using Newtonsoft.Json; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Notifications.Xbmc -{ - public class XbmcSettingsValidator : AbstractValidator - { - public XbmcSettingsValidator() - { - RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.DisplayTime).GreaterThanOrEqualTo(2); - } - } - - public class XbmcSettings : IProviderConfig - { - private static readonly XbmcSettingsValidator Validator = new XbmcSettingsValidator(); - - public XbmcSettings() - { - Port = 8080; - DisplayTime = 5; - } - - [FieldDefinition(0, Label = "Host")] - public string Host { get; set; } - - [FieldDefinition(1, Label = "Port")] - public int Port { get; set; } - - [FieldDefinition(2, Label = "Username")] - public string Username { get; set; } - - [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] - public string Password { get; set; } - - [DefaultValue(5)] - [FieldDefinition(4, Label = "Display Time", HelpText = "How long the notification will be displayed for (In seconds)")] - public int DisplayTime { get; set; } - - [FieldDefinition(5, Label = "GUI Notification", Type = FieldType.Checkbox)] - public bool Notify { get; set; } - - [FieldDefinition(6, Label = "Update Library", HelpText = "Update Library on Download & Rename?", Type = FieldType.Checkbox)] - public bool UpdateLibrary { get; set; } - - [FieldDefinition(7, Label = "Clean Library", HelpText = "Clean Library after update?", Type = FieldType.Checkbox)] - public bool CleanLibrary { get; set; } - - [FieldDefinition(8, Label = "Always Update", HelpText = "Update Library even when a file is playing?", Type = FieldType.Checkbox)] - public bool AlwaysUpdate { get; set; } - - [JsonIgnore] - public string Address => string.Format("{0}:{1}", Host, Port); - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index a1d8ca2f4..a548e4078 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -17,12 +17,11 @@ namespace NzbDrone.Core.Organizer { public interface IBuildFileNames { - string BuildTrackFileName(List tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig namingConfig = null, List preferredWords = null); - string BuildTrackFilePath(Artist artist, Album album, string fileName, string extension); - string BuildAlbumPath(Artist artist, Album album); + string BuildTrackFileName(Author artist, Book album, BookFile trackFile, NamingConfig namingConfig = null, List preferredWords = null); + string BuildTrackFilePath(Author artist, Book album, string fileName, string extension); + string BuildAlbumPath(Author artist, Book album); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); - string GetArtistFolder(Artist artist, NamingConfig namingConfig = null); - string GetAlbumFolder(Artist artist, Album album, NamingConfig namingConfig = null); + string GetArtistFolder(Author artist, NamingConfig namingConfig = null); } public class FileNameBuilder : IBuildFileNames @@ -84,7 +83,7 @@ namespace NzbDrone.Core.Organizer _logger = logger; } - public string BuildTrackFileName(List tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig namingConfig = null, List preferredWords = null) + public string BuildTrackFileName(Author artist, Book album, BookFile trackFile, NamingConfig namingConfig = null, List preferredWords = null) { if (namingConfig == null) { @@ -96,32 +95,20 @@ namespace NzbDrone.Core.Organizer return GetOriginalFileName(trackFile); } - if (namingConfig.StandardTrackFormat.IsNullOrWhiteSpace() || namingConfig.MultiDiscTrackFormat.IsNullOrWhiteSpace()) + if (namingConfig.StandardTrackFormat.IsNullOrWhiteSpace()) { - throw new NamingFormatException("Standard and Multi track formats cannot be empty"); + throw new NamingFormatException("File name format cannot be empty"); } var pattern = namingConfig.StandardTrackFormat; - if (tracks.First().AlbumRelease.Value.Media.Count() > 1) - { - pattern = namingConfig.MultiDiscTrackFormat; - } - var subFolders = pattern.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); var safePattern = subFolders.Aggregate("", (current, folderLevel) => Path.Combine(current, folderLevel)); var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - tracks = tracks.OrderBy(e => e.AlbumReleaseId).ThenBy(e => e.TrackNumber).ToList(); - - safePattern = FormatTrackNumberTokens(safePattern, "", tracks); - safePattern = FormatMediumNumberTokens(safePattern, "", tracks); - AddArtistTokens(tokenHandlers, artist); AddAlbumTokens(tokenHandlers, album); - AddMediumTokens(tokenHandlers, tracks.First().AlbumRelease.Value.Media.SingleOrDefault(m => m.Number == tracks.First().MediumNumber)); - AddTrackTokens(tokenHandlers, tracks); AddTrackFileTokens(tokenHandlers, trackFile); AddQualityTokens(tokenHandlers, artist, trackFile); AddMediaInfoTokens(tokenHandlers, trackFile); @@ -134,7 +121,7 @@ namespace NzbDrone.Core.Organizer return fileName; } - public string BuildTrackFilePath(Artist artist, Album album, string fileName, string extension) + public string BuildTrackFilePath(Author artist, Book album, string fileName, string extension) { Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); @@ -143,19 +130,10 @@ namespace NzbDrone.Core.Organizer return Path.Combine(path, fileName + extension); } - public string BuildAlbumPath(Artist artist, Album album) + public string BuildAlbumPath(Author artist, Book album) { var path = artist.Path; - if (artist.AlbumFolder) - { - var albumFolder = GetAlbumFolder(artist, album); - - albumFolder = CleanFileName(albumFolder); - - path = Path.Combine(path, albumFolder); - } - return path; } @@ -204,7 +182,7 @@ namespace NzbDrone.Core.Organizer return basicNamingConfig; } - public string GetArtistFolder(Artist artist, NamingConfig namingConfig = null) + public string GetArtistFolder(Author artist, NamingConfig namingConfig = null) { if (namingConfig == null) { @@ -218,21 +196,6 @@ namespace NzbDrone.Core.Organizer return CleanFolderName(ReplaceTokens(namingConfig.ArtistFolderFormat, tokenHandlers, namingConfig)); } - public string GetAlbumFolder(Artist artist, Album album, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddAlbumTokens(tokenHandlers, album); - AddArtistTokens(tokenHandlers, artist); - - return CleanFolderName(ReplaceTokens(namingConfig.AlbumFolderFormat, tokenHandlers, namingConfig)); - } - public static string CleanTitle(string title) { title = title.Replace("&", "and"); @@ -267,7 +230,7 @@ namespace NzbDrone.Core.Organizer return name.Trim(' ', '.'); } - private void AddArtistTokens(Dictionary> tokenHandlers, Artist artist) + private void AddArtistTokens(Dictionary> tokenHandlers, Author artist) { tokenHandlers["{Artist Name}"] = m => artist.Name; tokenHandlers["{Artist CleanName}"] = m => CleanTitle(artist.Name); @@ -279,12 +242,11 @@ namespace NzbDrone.Core.Organizer } } - private void AddAlbumTokens(Dictionary> tokenHandlers, Album album) + private void AddAlbumTokens(Dictionary> tokenHandlers, Book album) { tokenHandlers["{Album Title}"] = m => album.Title; tokenHandlers["{Album CleanTitle}"] = m => CleanTitle(album.Title); tokenHandlers["{Album TitleThe}"] = m => TitleThe(album.Title); - tokenHandlers["{Album Type}"] = m => album.AlbumType; if (album.Disambiguation != null) { @@ -301,25 +263,14 @@ namespace NzbDrone.Core.Organizer } } - private void AddMediumTokens(Dictionary> tokenHandlers, Medium medium) - { - tokenHandlers["{Medium Format}"] = m => medium.Format; - } - - private void AddTrackTokens(Dictionary> tokenHandlers, List tracks) - { - tokenHandlers["{Track Title}"] = m => GetTrackTitle(tracks, "+"); - tokenHandlers["{Track CleanTitle}"] = m => CleanTitle(GetTrackTitle(tracks, "and")); - } - - private void AddTrackFileTokens(Dictionary> tokenHandlers, TrackFile trackFile) + private void AddTrackFileTokens(Dictionary> tokenHandlers, BookFile trackFile) { tokenHandlers["{Original Title}"] = m => GetOriginalTitle(trackFile); tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(trackFile); tokenHandlers["{Release Group}"] = m => trackFile.ReleaseGroup ?? m.DefaultValue("Readarr"); } - private void AddQualityTokens(Dictionary> tokenHandlers, Artist artist, TrackFile trackFile) + private void AddQualityTokens(Dictionary> tokenHandlers, Author artist, BookFile trackFile) { var qualityTitle = _qualityDefinitionService.Get(trackFile.Quality.Quality).Title; var qualityProper = GetQualityProper(trackFile.Quality); @@ -332,7 +283,7 @@ namespace NzbDrone.Core.Organizer //tokenHandlers["{Quality Real}"] = m => qualityReal; } - private void AddMediaInfoTokens(Dictionary> tokenHandlers, TrackFile trackFile) + private void AddMediaInfoTokens(Dictionary> tokenHandlers, BookFile trackFile) { if (trackFile.MediaInfo == null) { @@ -354,7 +305,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{MediaInfo AudioSampleRate}"] = m => MediaInfoFormatter.FormatAudioSampleRate(trackFile.MediaInfo); } - private void AddPreferredWords(Dictionary> tokenHandlers, Artist artist, TrackFile trackFile, List preferredWords = null) + private void AddPreferredWords(Dictionary> tokenHandlers, Author artist, BookFile trackFile, List preferredWords = null) { if (preferredWords == null) { @@ -414,45 +365,6 @@ namespace NzbDrone.Core.Organizer return replacementText; } - private string FormatTrackNumberTokens(string basePattern, string formatPattern, List tracks) - { - var pattern = string.Empty; - - for (int i = 0; i < tracks.Count; i++) - { - var patternToReplace = i == 0 ? basePattern : formatPattern; - - pattern += TrackRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["track"].Value, tracks[i].AbsoluteTrackNumber)); - } - - return pattern; - } - - private string FormatMediumNumberTokens(string basePattern, string formatPattern, List tracks) - { - var pattern = string.Empty; - - for (int i = 0; i < tracks.Count; i++) - { - var patternToReplace = i == 0 ? basePattern : formatPattern; - - pattern += MediumRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["medium"].Value, tracks[i].MediumNumber)); - } - - return pattern; - } - - private string ReplaceNumberToken(string token, int value) - { - var split = token.Trim('{', '}').Split(':'); - if (split.Length == 1) - { - return value.ToString("0"); - } - - return value.ToString(split[1]); - } - private TrackFormat[] GetTrackFormat(string pattern) { return _trackFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType() @@ -464,36 +376,6 @@ namespace NzbDrone.Core.Organizer }).ToArray()); } - private string GetTrackTitle(List tracks, string separator) - { - separator = string.Format(" {0} ", separator.Trim()); - - if (tracks.Count == 1) - { - return tracks.First().Title.TrimEnd(TrackTitleTrimCharacters); - } - - var titles = tracks.Select(c => c.Title.TrimEnd(TrackTitleTrimCharacters)) - .Select(CleanupTrackTitle) - .Distinct() - .ToList(); - - if (titles.All(t => t.IsNullOrWhiteSpace())) - { - titles = tracks.Select(c => c.Title.TrimEnd(TrackTitleTrimCharacters)) - .Distinct() - .ToList(); - } - - return string.Join(separator, titles); - } - - private string CleanupTrackTitle(string title) - { - //this will remove (1),(2) from the end of multi part episodes. - return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); - } - private string GetQualityProper(QualityModel quality) { if (quality.Revision.Version > 1) @@ -509,16 +391,7 @@ namespace NzbDrone.Core.Organizer return string.Empty; } - //private string GetQualityReal(Series series, QualityModel quality) - //{ - // if (quality.Revision.Real > 0) - // { - // return "REAL"; - // } - - // return string.Empty; - //} - private string GetOriginalTitle(TrackFile trackFile) + private string GetOriginalTitle(BookFile trackFile) { if (trackFile.SceneName.IsNullOrWhiteSpace()) { @@ -528,7 +401,7 @@ namespace NzbDrone.Core.Organizer return trackFile.SceneName; } - private string GetOriginalFileName(TrackFile trackFile) + private string GetOriginalFileName(BookFile trackFile) { return Path.GetFileNameWithoutExtension(trackFile.Path); } diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index d2a1b8ce0..2f8a3bd6c 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -11,90 +11,37 @@ namespace NzbDrone.Core.Organizer SampleResult GetStandardTrackSample(NamingConfig nameSpec); SampleResult GetMultiDiscTrackSample(NamingConfig nameSpec); string GetArtistFolderSample(NamingConfig nameSpec); - string GetAlbumFolderSample(NamingConfig nameSpec); } public class FileNameSampleService : IFilenameSampleService { private readonly IBuildFileNames _buildFileNames; - private static Artist _standardArtist; - private static Album _standardAlbum; - private static AlbumRelease _singleRelease; - private static AlbumRelease _multiRelease; - private static Track _track1; - private static List _singleTrack; - private static TrackFile _singleTrackFile; + private static Author _standardArtist; + private static Book _standardAlbum; + private static BookFile _singleTrackFile; private static List _preferredWords; public FileNameSampleService(IBuildFileNames buildFileNames) { _buildFileNames = buildFileNames; - _standardArtist = new Artist + _standardArtist = new Author { - Metadata = new ArtistMetadata + Metadata = new AuthorMetadata { - Name = "The Artist Name", - Disambiguation = "US Rock Band" + Name = "The Author Name", + Disambiguation = "US Author" } }; - _standardAlbum = new Album + _standardAlbum = new Book { - Title = "The Album Title", + Title = "The Book Title", ReleaseDate = System.DateTime.Today, - AlbumType = "Album", - Disambiguation = "The Best Album", + Disambiguation = "First Book" }; - _singleRelease = new AlbumRelease - { - Album = _standardAlbum, - Media = new List - { - new Medium - { - Name = "CD 1: First Years", - Format = "CD", - Number = 1 - } - }, - Monitored = true - }; - - _multiRelease = new AlbumRelease - { - Album = _standardAlbum, - Media = new List - { - new Medium - { - Name = "CD 1: First Years", - Format = "CD", - Number = 1 - }, - new Medium - { - Name = "CD 2: Second Best", - Format = "CD", - Number = 2 - } - }, - Monitored = true - }; - - _track1 = new Track - { - AlbumRelease = _singleRelease, - AbsoluteTrackNumber = 3, - MediumNumber = 1, - - Title = "Track Title (1)", - }; - - _singleTrack = new List { _track1 }; - var mediaInfo = new MediaInfoModel() { AudioFormat = "Flac Audio", @@ -104,9 +51,9 @@ namespace NzbDrone.Core.Organizer AudioSampleRate = 44100 }; - _singleTrackFile = new TrackFile + _singleTrackFile = new BookFile { - Quality = new QualityModel(Quality.MP3_256, new Revision(2)), + Quality = new QualityModel(Quality.MP3_320, new Revision(2)), Path = "/music/Artist.Name.Album.Name.TrackNum.Track.Title.MP3256.mp3", SceneName = "Artist.Name.Album.Name.TrackNum.Track.Title.MP3256", ReleaseGroup = "RlsGrp", @@ -121,14 +68,11 @@ namespace NzbDrone.Core.Organizer public SampleResult GetStandardTrackSample(NamingConfig nameSpec) { - _track1.AlbumRelease = _singleRelease; - var result = new SampleResult { - FileName = BuildTrackSample(_singleTrack, _standardArtist, _standardAlbum, _singleTrackFile, nameSpec), + FileName = BuildTrackSample(_standardArtist, _standardAlbum, _singleTrackFile, nameSpec), Artist = _standardArtist, Album = _standardAlbum, - Tracks = _singleTrack, TrackFile = _singleTrackFile }; @@ -137,14 +81,11 @@ namespace NzbDrone.Core.Organizer public SampleResult GetMultiDiscTrackSample(NamingConfig nameSpec) { - _track1.AlbumRelease = _multiRelease; - var result = new SampleResult { - FileName = BuildTrackSample(_singleTrack, _standardArtist, _standardAlbum, _singleTrackFile, nameSpec), + FileName = BuildTrackSample(_standardArtist, _standardAlbum, _singleTrackFile, nameSpec), Artist = _standardArtist, Album = _standardAlbum, - Tracks = _singleTrack, TrackFile = _singleTrackFile }; @@ -156,16 +97,11 @@ namespace NzbDrone.Core.Organizer return _buildFileNames.GetArtistFolder(_standardArtist, nameSpec); } - public string GetAlbumFolderSample(NamingConfig nameSpec) - { - return _buildFileNames.GetAlbumFolder(_standardArtist, _standardAlbum, nameSpec); - } - - private string BuildTrackSample(List tracks, Artist artist, Album album, TrackFile trackFile, NamingConfig nameSpec) + private string BuildTrackSample(Author artist, Book album, BookFile trackFile, NamingConfig nameSpec) { try { - return _buildFileNames.BuildTrackFileName(tracks, artist, album, trackFile, nameSpec, _preferredWords); + return _buildFileNames.BuildTrackFileName(artist, album, trackFile, nameSpec, _preferredWords); } catch (NamingFormatException) { diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index e7d7235b9..79461dd09 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -20,20 +20,12 @@ namespace NzbDrone.Core.Organizer ruleBuilder.SetValidator(new NotEmptyValidator(null)); return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.ArtistNameRegex)).WithMessage("Must contain Artist name"); } - - public static IRuleBuilderOptions ValidAlbumFolderFormat(this IRuleBuilder ruleBuilder) - { - ruleBuilder.SetValidator(new NotEmptyValidator(null)); - return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.AlbumTitleRegex)).WithMessage("Must contain Album title"); - - //.SetValidator(new RegularExpressionValidator(FileNameBuilder.ReleaseDateRegex)).WithMessage("Must contain Release year"); - } } public class ValidStandardTrackFormatValidator : PropertyValidator { public ValidStandardTrackFormatValidator() - : base("Must contain Track Title and Track numbers OR Original Title") + : base("Must contain Album Title") { } @@ -41,9 +33,7 @@ namespace NzbDrone.Core.Organizer { var value = context.PropertyValue as string; - if (!(FileNameBuilder.TrackTitleRegex.IsMatch(value) && - FileNameBuilder.TrackRegex.IsMatch(value)) && - !FileNameValidation.OriginalTokenRegex.IsMatch(value)) + if (!FileNameBuilder.AlbumTitleRegex.IsMatch(value)) { return false; } diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 4226200d5..502528d61 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -1,3 +1,4 @@ +using System.IO; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Organizer @@ -8,17 +9,13 @@ namespace NzbDrone.Core.Organizer { RenameTracks = false, ReplaceIllegalCharacters = true, - StandardTrackFormat = "{Artist Name} - {Album Title} - {track:00} - {Track Title}", - MultiDiscTrackFormat = "{Medium Format} {medium:00}/{Artist Name} - {Album Title} - {track:00} - {Track Title}", + StandardTrackFormat = "{Album Title}" + Path.DirectorySeparatorChar + "{Artist Name} - {Album Title}", ArtistFolderFormat = "{Artist Name}", - AlbumFolderFormat = "{Album Title} ({Release Year})" }; public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } public string StandardTrackFormat { get; set; } - public string MultiDiscTrackFormat { get; set; } public string ArtistFolderFormat { get; set; } - public string AlbumFolderFormat { get; set; } } } diff --git a/src/NzbDrone.Core/Organizer/SampleResult.cs b/src/NzbDrone.Core/Organizer/SampleResult.cs index fb85df202..23b53961a 100644 --- a/src/NzbDrone.Core/Organizer/SampleResult.cs +++ b/src/NzbDrone.Core/Organizer/SampleResult.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Music; @@ -7,9 +6,8 @@ namespace NzbDrone.Core.Organizer public class SampleResult { public string FileName { get; set; } - public Artist Artist { get; set; } - public Album Album { get; set; } - public List Tracks { get; set; } - public TrackFile TrackFile { get; set; } + public Author Artist { get; set; } + public Book Album { get; set; } + public BookFile TrackFile { get; set; } } } diff --git a/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs b/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs index df82059a0..59bb9ec59 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs @@ -31,29 +31,20 @@ namespace NzbDrone.Core.Parser.Model public List LocalTracks { get; set; } public int TrackCount => LocalTracks.Count; - public TrackMapping TrackMapping { get; set; } public Distance Distance { get; set; } - public AlbumRelease AlbumRelease { get; set; } + public Book Book { get; set; } public List ExistingTracks { get; set; } public bool NewDownload { get; set; } public void PopulateMatch() { - if (AlbumRelease != null) + if (Book != null) { LocalTracks = LocalTracks.Concat(ExistingTracks).DistinctBy(x => x.Path).ToList(); foreach (var localTrack in LocalTracks) { - localTrack.Release = AlbumRelease; - localTrack.Album = AlbumRelease.Album.Value; - localTrack.Artist = localTrack.Album.Artist.Value; - - if (TrackMapping.Mapping.ContainsKey(localTrack)) - { - var track = TrackMapping.Mapping[localTrack].Item1; - localTrack.Tracks = new List { track }; - localTrack.Distance = TrackMapping.Mapping[localTrack].Item2; - } + localTrack.Album = Book; + localTrack.Artist = Book.Author.Value; } } } @@ -63,16 +54,4 @@ namespace NzbDrone.Core.Parser.Model return "[" + string.Join(", ", LocalTracks.Select(x => Path.GetDirectoryName(x.Path)).Distinct()) + "]"; } } - - public class TrackMapping - { - public TrackMapping() - { - Mapping = new Dictionary>(); - } - - public Dictionary> Mapping { get; set; } - public List LocalExtra { get; set; } - public List MBExtra { get; set; } - } } diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index 0c89f196c..e90b91aa1 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -8,11 +8,6 @@ namespace NzbDrone.Core.Parser.Model { public class LocalTrack { - public LocalTrack() - { - Tracks = new List(); - } - public string Path { get; set; } public long Size { get; set; } public DateTime Modified { get; set; } @@ -20,10 +15,8 @@ namespace NzbDrone.Core.Parser.Model public ParsedTrackInfo FolderTrackInfo { get; set; } public ParsedAlbumInfo DownloadClientAlbumInfo { get; set; } public List AcoustIdResults { get; set; } - public Artist Artist { get; set; } - public Album Album { get; set; } - public AlbumRelease Release { get; set; } - public List Tracks { get; set; } + public Author Artist { get; set; } + public Book Album { get; set; } public Distance Distance { get; set; } public QualityModel Quality { get; set; } public bool ExistingFile { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs index 567564695..b5d804bf3 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs @@ -11,7 +11,11 @@ namespace NzbDrone.Core.Parser.Model public string CleanTitle { get; set; } public string ArtistTitle { get; set; } public string AlbumTitle { get; set; } - public ArtistTitleInfo ArtistTitleInfo { get; set; } + public string SeriesTitle { get; set; } + public string SeriesIndex { get; set; } + public string Isbn { get; set; } + public string Asin { get; set; } + public string GoodreadsId { get; set; } public string ArtistMBId { get; set; } public string AlbumMBId { get; set; } public string ReleaseMBId { get; set; } @@ -21,13 +25,16 @@ namespace NzbDrone.Core.Parser.Model public int DiscCount { get; set; } public IsoCountry Country { get; set; } public uint Year { get; set; } + public string Publisher { get; set; } public string Label { get; set; } + public string Source { get; set; } public string CatalogNumber { get; set; } public string Disambiguation { get; set; } public TimeSpan Duration { get; set; } public QualityModel Quality { get; set; } public MediaInfoModel MediaInfo { get; set; } public int[] TrackNumbers { get; set; } + public string Language { get; set; } public string ReleaseGroup { get; set; } public string ReleaseHash { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs b/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs index ea4b35190..154fbc573 100644 --- a/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs +++ b/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs @@ -10,15 +10,15 @@ namespace NzbDrone.Core.Parser.Model { public ReleaseInfo Release { get; set; } public ParsedAlbumInfo ParsedAlbumInfo { get; set; } - public Artist Artist { get; set; } - public List Albums { get; set; } + public Author Artist { get; set; } + public List Albums { get; set; } public bool DownloadAllowed { get; set; } public TorrentSeedConfiguration SeedConfiguration { get; set; } public int PreferredWordScore { get; set; } public RemoteAlbum() { - Albums = new List(); + Albums = new List(); } public bool IsRecentAlbum() diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 8383fd7df..3d8401e8b 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -195,7 +195,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex YearInTitleRegex = new Regex(@"^(?.+?)(?:\W|_)?(?<year>\d{4})", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|\|)+", RegexOptions.Compiled); + private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|\(|\)|\[|\]|\|)+", RegexOptions.Compiled); private static readonly Regex PunctuationRegex = new Regex(@"[^\w\s]", RegexOptions.Compiled); private static readonly Regex CommonWordRegex = new Regex(@"\b(a|an|the|and|or|of)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -219,6 +219,8 @@ namespace NzbDrone.Core.Parser private static readonly Regex AfterDashRegex = new Regex(@"[-:].*", RegexOptions.Compiled); + private static readonly Regex CalibreIdRegex = new Regex(@"\((?<id>\d+)\)", RegexOptions.Compiled); + public static ParsedTrackInfo ParseMusicPath(string path) { var fileInfo = new FileInfo(path); @@ -291,7 +293,7 @@ namespace NzbDrone.Core.Parser if (result != null) { - result.Quality = QualityParser.ParseQuality(title, null, 0); + result.Quality = QualityParser.ParseQuality(title); Logger.Debug("Quality parsed: {0}", result.Quality); return result; @@ -317,7 +319,7 @@ namespace NzbDrone.Core.Parser return null; } - public static ParsedAlbumInfo ParseAlbumTitleWithSearchCriteria(string title, Artist artist, List<Album> album) + public static ParsedAlbumInfo ParseAlbumTitleWithSearchCriteria(string title, Author artist, List<Book> album) { try { @@ -341,47 +343,39 @@ namespace NzbDrone.Core.Parser simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle); - var escapedArtist = Regex.Escape(artistName.RemoveAccent()).Replace(@"\ ", @"[\W_]"); - var escapedAlbums = string.Join("|", album.Select(s => Regex.Escape(s.Title.RemoveAccent())).ToList()).Replace(@"\ ", @"[\W_]"); + var bestAlbum = album.OrderByDescending(x => simpleTitle.FuzzyContains(x.Title)).First(); - var releaseRegex = new Regex(@"^(\W*|\b)(?<artist>" + escapedArtist + @")(\W*|\b).*(\W*|\b)(?<album>" + escapedAlbums + @")(\W*|\b)", RegexOptions.IgnoreCase); + var foundArtist = GetTitleFuzzy(simpleTitle, artistName, out var remainder); + var foundAlbum = GetTitleFuzzy(remainder, bestAlbum.Title, out _); - var match = releaseRegex.Matches(simpleTitle); + Logger.Trace($"Found {foundArtist} - {foundAlbum} with fuzzy parser"); - if (match.Count != 0) + if (foundArtist == null || foundAlbum == null) { - try - { - var result = ParseAlbumMatchCollection(match); - - if (result != null) - { - result.Quality = QualityParser.ParseQuality(title, null, 0); - Logger.Debug("Quality parsed: {0}", result.Quality); + return null; + } - result.ReleaseGroup = ParseReleaseGroup(releaseTitle); + var result = new ParsedAlbumInfo + { + ArtistName = foundArtist, + ArtistTitleInfo = GetArtistTitleInfo(foundArtist), + AlbumTitle = foundAlbum + }; - var subGroup = GetSubGroup(match); - if (!subGroup.IsNullOrWhiteSpace()) - { - result.ReleaseGroup = subGroup; - } + try + { + result.Quality = QualityParser.ParseQuality(title); + Logger.Debug("Quality parsed: {0}", result.Quality); - Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup); + result.ReleaseGroup = ParseReleaseGroup(releaseTitle); - result.ReleaseHash = GetReleaseHash(match); - if (!result.ReleaseHash.IsNullOrWhiteSpace()) - { - Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash); - } + Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup); - return result; - } - } - catch (InvalidDateException ex) - { - Logger.Debug(ex, ex.Message); - } + return result; + } + catch (InvalidDateException ex) + { + Logger.Debug(ex, ex.Message); } } catch (Exception e) @@ -396,6 +390,86 @@ namespace NzbDrone.Core.Parser return null; } + private static string GetTitleFuzzy(string report, string name, out string remainder) + { + remainder = report; + + Logger.Trace($"Finding '{name}' in '{report}'"); + var loc = report.ToLowerInvariant().FuzzyFind(name.ToLowerInvariant(), 0.6); + + if (loc == -1) + { + return null; + } + + Logger.Trace($"start '{loc}'"); + + var boundaries = WordDelimiterRegex.Matches(report); + + if (boundaries.Count == 0) + { + return null; + } + + var starts = new List<int>(); + var finishes = new List<int>(); + + if (boundaries[0].Index == 0) + { + starts.Add(boundaries[0].Length); + } + else + { + starts.Add(0); + } + + foreach (Match match in boundaries) + { + var start = match.Index + match.Length; + if (start < report.Length) + { + starts.Add(start); + } + + var finish = match.Index - 1; + if (finish >= 0) + { + finishes.Add(finish); + } + } + + var lastMatch = boundaries[boundaries.Count - 1]; + if (lastMatch.Index + lastMatch.Length < report.Length) + { + finishes.Add(report.Length - 1); + } + + Logger.Trace(starts.ConcatToString(x => x.ToString())); + Logger.Trace(finishes.ConcatToString(x => x.ToString())); + + var wordStart = starts.OrderBy(x => Math.Abs(x - loc)).First(); + var wordEnd = finishes.OrderBy(x => Math.Abs(x - (loc + name.Length))).First(); + + var found = report.Substring(wordStart, wordEnd - wordStart + 1); + + if (found.ToLowerInvariant().FuzzyMatch(name.ToLowerInvariant()) >= 0.8) + { + remainder = report.Remove(wordStart, wordEnd - wordStart + 1); + return found.Replace('.', ' ').Replace('_', ' '); + } + + return null; + } + + public static int ParseCalibreId(this string path) + { + var bookFolder = path.GetParentPath(); + + var match = CalibreIdRegex.Match(bookFolder); + + return match.Success ? int.Parse(match.Groups["id"].Value) : 0; + } + public static ParsedAlbumInfo ParseAlbumTitle(string title) { try @@ -450,7 +524,7 @@ namespace NzbDrone.Core.Parser if (result != null) { - result.Quality = QualityParser.ParseQuality(title, null, 0); + result.Quality = QualityParser.ParseQuality(title); Logger.Debug("Quality parsed: {0}", result.Quality); result.ReleaseGroup = ParseReleaseGroup(releaseTitle); @@ -562,7 +636,7 @@ namespace NzbDrone.Core.Parser title = FileExtensionRegex.Replace(title, m => { var extension = m.Value.ToLower(); - if (MediaFiles.MediaFileExtensions.Extensions.Contains(extension) || new[] { ".par2", ".nzb" }.Contains(extension)) + if (MediaFiles.MediaFileExtensions.AllExtensions.Contains(extension) || new[] { ".par2", ".nzb" }.Contains(extension)) { return string.Empty; } @@ -653,7 +727,6 @@ namespace NzbDrone.Core.Parser ParsedTrackInfo result = new ParsedTrackInfo(); result.ArtistTitle = artistName; - result.ArtistTitleInfo = GetArtistTitleInfo(result.ArtistTitle); Logger.Debug("Track Parsed. {0}", result); return result; diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index e95d2ef32..3d765c6fe 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -13,38 +13,37 @@ namespace NzbDrone.Core.Parser { public interface IParsingService { - Artist GetArtist(string title); - Artist GetArtistFromTag(string file); + Author GetArtist(string title); + Author GetArtistFromTag(string file); RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, SearchCriteriaBase searchCriteria = null); - RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, int artistId, IEnumerable<int> albumIds); - List<Album> GetAlbums(ParsedAlbumInfo parsedAlbumInfo, Artist artist, SearchCriteriaBase searchCriteria = null); + RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, int authorId, IEnumerable<int> bookIds); + List<Book> GetAlbums(ParsedAlbumInfo parsedAlbumInfo, Author artist, SearchCriteriaBase searchCriteria = null); + + ParsedAlbumInfo ParseAlbumTitleFuzzy(string title); // Music stuff here - Album GetLocalAlbum(string filename, Artist artist); + Book GetLocalAlbum(string filename, Author artist); } public class ParsingService : IParsingService { private readonly IArtistService _artistService; private readonly IAlbumService _albumService; - private readonly ITrackService _trackService; private readonly IMediaFileService _mediaFileService; private readonly Logger _logger; - public ParsingService(ITrackService trackService, - IArtistService artistService, + public ParsingService(IArtistService artistService, IAlbumService albumService, IMediaFileService mediaFileService, Logger logger) { _albumService = albumService; _artistService = artistService; - _trackService = trackService; _mediaFileService = mediaFileService; _logger = logger; } - public Artist GetArtist(string title) + public Author GetArtist(string title) { var parsedAlbumInfo = Parser.ParseAlbumTitle(title); @@ -64,11 +63,11 @@ namespace NzbDrone.Core.Parser return artistInfo; } - public Artist GetArtistFromTag(string file) + public Author GetArtistFromTag(string file) { var parsedTrackInfo = Parser.ParseMusicPath(file); - var artist = new Artist(); + var artist = new Author(); if (parsedTrackInfo.ArtistMBId.IsNotNullOrWhiteSpace()) { @@ -116,17 +115,17 @@ namespace NzbDrone.Core.Parser return remoteAlbum; } - public List<Album> GetAlbums(ParsedAlbumInfo parsedAlbumInfo, Artist artist, SearchCriteriaBase searchCriteria = null) + public List<Book> GetAlbums(ParsedAlbumInfo parsedAlbumInfo, Author artist, SearchCriteriaBase searchCriteria = null) { var albumTitle = parsedAlbumInfo.AlbumTitle; - var result = new List<Album>(); + var result = new List<Book>(); if (parsedAlbumInfo.AlbumTitle == null) { - return new List<Album>(); + return new List<Book>(); } - Album albumInfo = null; + Book albumInfo = null; if (parsedAlbumInfo.Discography) { @@ -157,13 +156,13 @@ namespace NzbDrone.Core.Parser if (albumInfo == null) { // TODO: Search by Title and Year instead of just Title when matching - albumInfo = _albumService.FindByTitle(artist.ArtistMetadataId, parsedAlbumInfo.AlbumTitle); + albumInfo = _albumService.FindByTitle(artist.AuthorMetadataId, parsedAlbumInfo.AlbumTitle); } if (albumInfo == null) { _logger.Debug("Trying inexact album match for {0}", parsedAlbumInfo.AlbumTitle); - albumInfo = _albumService.FindByTitleInexact(artist.ArtistMetadataId, parsedAlbumInfo.AlbumTitle); + albumInfo = _albumService.FindByTitleInexact(artist.AuthorMetadataId, parsedAlbumInfo.AlbumTitle); } if (albumInfo != null) @@ -178,19 +177,19 @@ namespace NzbDrone.Core.Parser return result; } - public RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, int artistId, IEnumerable<int> albumIds) + public RemoteAlbum Map(ParsedAlbumInfo parsedAlbumInfo, int authorId, IEnumerable<int> bookIds) { return new RemoteAlbum { ParsedAlbumInfo = parsedAlbumInfo, - Artist = _artistService.GetArtist(artistId), - Albums = _albumService.GetAlbums(albumIds) + Artist = _artistService.GetArtist(authorId), + Albums = _albumService.GetAlbums(bookIds) }; } - private Artist GetArtist(ParsedAlbumInfo parsedAlbumInfo, SearchCriteriaBase searchCriteria) + private Author GetArtist(ParsedAlbumInfo parsedAlbumInfo, SearchCriteriaBase searchCriteria) { - Artist artist = null; + Author artist = null; if (searchCriteria != null) { @@ -217,7 +216,48 @@ namespace NzbDrone.Core.Parser return artist; } - public Album GetLocalAlbum(string filename, Artist artist) + public ParsedAlbumInfo ParseAlbumTitleFuzzy(string title) + { + var bestScore = 0.0; + + Author bestAuthor = null; + Book bestBook = null; + + var possibleAuthors = _artistService.GetReportCandidates(title); + + foreach (var author in possibleAuthors) + { + _logger.Trace($"Trying possible author {author}"); + + var authorMatch = title.FuzzyMatch(author.Metadata.Value.Name, 0.5); + var possibleBooks = _albumService.GetCandidates(author.AuthorMetadataId, title); + + foreach (var book in possibleBooks) + { + var bookMatch = title.FuzzyMatch(book.Title, 0.5); + var score = (authorMatch.Item2 + bookMatch.Item2) / 2; + + _logger.Trace($"Book {book} has score {score}"); + + if (score > bestScore) + { + bestAuthor = author; + bestBook = book; + } + } + } + + _logger.Trace($"Best match: {bestAuthor} {bestBook}"); + + if (bestAuthor != null) + { + return Parser.ParseAlbumTitleWithSearchCriteria(title, bestAuthor, new List<Book> { bestBook }); + } + + return null; + } + + public Book GetLocalAlbum(string filename, Author artist) { if (Path.HasExtension(filename)) { @@ -226,10 +266,10 @@ namespace NzbDrone.Core.Parser var tracksInAlbum = _mediaFileService.GetFilesByArtist(artist.Id) .FindAll(s => Path.GetDirectoryName(s.Path) == filename) - .DistinctBy(s => s.AlbumId) + .DistinctBy(s => s.BookId) .ToList(); - return tracksInAlbum.Count == 1 ? _albumService.GetAlbum(tracksInAlbum.First().AlbumId) : null; + return tracksInAlbum.Count == 1 ? _albumService.GetAlbum(tracksInAlbum.First().BookId) : null; } } } diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 4125c8c6a..a0aee4ddd 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -25,24 +25,10 @@ namespace NzbDrone.Core.Parser private static readonly Regex RealRegex = new Regex(@"\b(?<real>REAL)\b", RegexOptions.Compiled); - private static readonly Regex BitRateRegex = new Regex(@"\b(?:(?<B096>96[ ]?kbps|96|[\[\(].*96.*[\]\)])| - (?<B128>128[ ]?kbps|128|[\[\(].*128.*[\]\)])| - (?<B160>160[ ]?kbps|160|[\[\(].*160.*[\]\)]|q5)| - (?<B192>192[ ]?kbps|192|[\[\(].*192.*[\]\)]|q6)| - (?<B224>224[ ]?kbps|224|[\[\(].*224.*[\]\)]|q7)| - (?<B256>256[ ]?kbps|256|itunes\splus|[\[\(].*256.*[\]\)]|q8)| - (?<B320>320[ ]?kbps|320|[\[\(].*320.*[\]\)]|q9)| - (?<B500>500[ ]?kbps|500|[\[\(].*500.*[\]\)]|q10)| - (?<VBRV0>V0[ ]?kbps|V0|[\[\(].*V0.*[\]\)])| - (?<VBRV2>V2[ ]?kbps|V2|[\[\(].*V2.*[\]\)]))\b", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private static readonly Regex SampleSizeRegex = new Regex(@"\b(?:(?<S24>24[ ]bit|24bit|[\[\(].*24bit.*[\]\)]))"); - - private static readonly Regex CodecRegex = new Regex(@"\b(?:(?<MP1>MPEG Version \d(.5)? Audio, Layer 1|MP1)|(?<MP2>MPEG Version \d(.5)? Audio, Layer 2|MP2)|(?<MP3VBR>MP3.*VBR|MPEG Version \d(.5)? Audio, Layer 3 vbr)|(?<MP3CBR>MP3|MPEG Version \d(.5)? Audio, Layer 3)|(?<FLAC>flac)|(?<WAVPACK>wavpack|wv)|(?<ALAC>alac)|(?<WMA>WMA\d?)|(?<WAV>WAV|PCM)|(?<AAC>M4A|M4P|M4B|AAC|mp4a|MPEG-4 Audio(?!.*alac))|(?<OGG>OGG|OGA|Vorbis))\b|(?<APE>monkey's audio|[\[|\(].*\bape\b.*[\]|\)])|(?<OPUS>Opus Version \d(.5)? Audio|[\[|\(].*\bopus\b.*[\]|\)])", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static QualityModel ParseQuality(string name, string desc, int fileBitrate, int fileSampleSize = 0) + private static readonly Regex CodecRegex = new Regex(@"\b(?:(?<PDF>PDF)|(?<MOBI>MOBI)|(?<EPUB>EPUB)|(?<AZW3>AZW3?)|(?<MP1>MPEG Version \d(.5)? Audio, Layer 1|MP1)|(?<MP2>MPEG Version \d(.5)? Audio, Layer 2|MP2)|(?<MP3VBR>MP3.*VBR|MPEG Version \d(.5)? Audio, Layer 3 vbr)|(?<MP3CBR>MP3|MPEG Version \d(.5)? Audio, Layer 3)|(?<FLAC>flac)|(?<WAVPACK>wavpack|wv)|(?<ALAC>alac)|(?<WMA>WMA\d?)|(?<WAV>WAV|PCM)|(?<AAC>M4A|M4P|M4B|AAC|mp4a|MPEG-4 Audio(?!.*alac))|(?<OGG>OGG|OGA|Vorbis))\b|(?<APE>monkey's audio|[\[|\(].*\bape\b.*[\]|\)])|(?<OPUS>Opus Version \d(.5)? Audio|[\[|\(].*\bopus\b.*[\]|\)])", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static QualityModel ParseQuality(string name, string desc = null) { Logger.Debug("Trying to parse quality for {0}", name); @@ -54,7 +40,7 @@ namespace NzbDrone.Core.Parser var descCodec = ParseCodec(desc, ""); Logger.Trace($"Got codec {descCodec}"); - result.Quality = FindQuality(descCodec, fileBitrate, fileSampleSize); + result.Quality = FindQuality(descCodec); if (result.Quality != Quality.Unknown) { @@ -64,164 +50,40 @@ namespace NzbDrone.Core.Parser } var codec = ParseCodec(normalizedName, name); - var bitrate = ParseBitRate(normalizedName); - var sampleSize = ParseSampleSize(normalizedName); switch (codec) { - case Codec.MP1: - case Codec.MP2: - result.Quality = Quality.Unknown; + case Codec.PDF: + result.Quality = Quality.PDF; break; - case Codec.MP3VBR: - if (bitrate == BitRate.VBRV0) - { - result.Quality = Quality.MP3_VBR; - } - else if (bitrate == BitRate.VBRV2) - { - result.Quality = Quality.MP3_VBR_V2; - } - else - { - result.Quality = Quality.Unknown; - } - + case Codec.EPUB: + result.Quality = Quality.EPUB; break; - case Codec.MP3CBR: - if (bitrate == BitRate.B096) - { - result.Quality = Quality.MP3_096; - } - else if (bitrate == BitRate.B128) - { - result.Quality = Quality.MP3_128; - } - else if (bitrate == BitRate.B160) - { - result.Quality = Quality.MP3_160; - } - else if (bitrate == BitRate.B192) - { - result.Quality = Quality.MP3_192; - } - else if (bitrate == BitRate.B256) - { - result.Quality = Quality.MP3_256; - } - else if (bitrate == BitRate.B320) - { - result.Quality = Quality.MP3_320; - } - else - { - result.Quality = Quality.Unknown; - } - + case Codec.MOBI: + result.Quality = Quality.MOBI; break; - case Codec.FLAC: - if (sampleSize == SampleSize.S24) - { - result.Quality = Quality.FLAC_24; - } - else - { - result.Quality = Quality.FLAC; - } - + case Codec.AZW3: + result.Quality = Quality.AZW3; break; + case Codec.FLAC: case Codec.ALAC: - result.Quality = Quality.ALAC; - break; case Codec.WAVPACK: - result.Quality = Quality.WAVPACK; + result.Quality = Quality.FLAC; break; + case Codec.MP1: + case Codec.MP2: + case Codec.MP3VBR: + case Codec.MP3CBR: case Codec.APE: - result.Quality = Quality.APE; - break; case Codec.WMA: - result.Quality = Quality.WMA; - break; case Codec.WAV: - result.Quality = Quality.WAV; - break; case Codec.AAC: - if (bitrate == BitRate.B192) - { - result.Quality = Quality.AAC_192; - } - else if (bitrate == BitRate.B256) - { - result.Quality = Quality.AAC_256; - } - else if (bitrate == BitRate.B320) - { - result.Quality = Quality.AAC_320; - } - else - { - result.Quality = Quality.AAC_VBR; - } - - break; case Codec.AACVBR: - result.Quality = Quality.AAC_VBR; - break; case Codec.OGG: case Codec.OPUS: - if (bitrate == BitRate.B160) - { - result.Quality = Quality.VORBIS_Q5; - } - else if (bitrate == BitRate.B192) - { - result.Quality = Quality.VORBIS_Q6; - } - else if (bitrate == BitRate.B224) - { - result.Quality = Quality.VORBIS_Q7; - } - else if (bitrate == BitRate.B256) - { - result.Quality = Quality.VORBIS_Q8; - } - else if (bitrate == BitRate.B320) - { - result.Quality = Quality.VORBIS_Q9; - } - else if (bitrate == BitRate.B500) - { - result.Quality = Quality.VORBIS_Q10; - } - else - { - result.Quality = Quality.Unknown; - } - + result.Quality = Quality.MP3_320; break; case Codec.Unknown: - if (bitrate == BitRate.B192) - { - result.Quality = Quality.MP3_192; - } - else if (bitrate == BitRate.B256) - { - result.Quality = Quality.MP3_256; - } - else if (bitrate == BitRate.B320) - { - result.Quality = Quality.MP3_320; - } - else if (bitrate == BitRate.VBR) - { - result.Quality = Quality.MP3_VBR_V2; - } - else - { - result.Quality = Quality.Unknown; - } - - break; default: result.Quality = Quality.Unknown; break; @@ -259,6 +121,26 @@ namespace NzbDrone.Core.Parser return Codec.Unknown; } + if (match.Groups["PDF"].Success) + { + return Codec.PDF; + } + + if (match.Groups["EPUB"].Success) + { + return Codec.EPUB; + } + + if (match.Groups["MOBI"].Success) + { + return Codec.MOBI; + } + + if (match.Groups["AZW3"].Success) + { + return Codec.AZW3; + } + if (match.Groups["FLAC"].Success) { return Codec.FLAC; @@ -327,287 +209,26 @@ namespace NzbDrone.Core.Parser return Codec.Unknown; } - private static BitRate ParseBitRate(string name) - { - //var nameWithNoSpaces = Regex.Replace(name, @"\s+", ""); - var match = BitRateRegex.Match(name); - - if (!match.Success) - { - return BitRate.Unknown; - } - - if (match.Groups["B096"].Success) - { - return BitRate.B096; - } - - if (match.Groups["B128"].Success) - { - return BitRate.B128; - } - - if (match.Groups["B160"].Success) - { - return BitRate.B160; - } - - if (match.Groups["B192"].Success) - { - return BitRate.B192; - } - - if (match.Groups["B224"].Success) - { - return BitRate.B224; - } - - if (match.Groups["B256"].Success) - { - return BitRate.B256; - } - - if (match.Groups["B320"].Success) - { - return BitRate.B320; - } - - if (match.Groups["B500"].Success) - { - return BitRate.B500; - } - - if (match.Groups["VBR"].Success) - { - return BitRate.VBR; - } - - if (match.Groups["VBRV0"].Success) - { - return BitRate.VBRV0; - } - - if (match.Groups["VBRV2"].Success) - { - return BitRate.VBRV2; - } - - return BitRate.Unknown; - } - - private static SampleSize ParseSampleSize(string name) - { - var match = SampleSizeRegex.Match(name); - - if (!match.Success) - { - return SampleSize.Unknown; - } - - if (match.Groups["S24"].Success) - { - return SampleSize.S24; - } - - return SampleSize.Unknown; - } - - private static Quality FindQuality(Codec codec, int bitrate, int sampleSize = 0) + private static Quality FindQuality(Codec codec) { switch (codec) { + case Codec.ALAC: + case Codec.FLAC: + case Codec.WAVPACK: + case Codec.WAV: + return Quality.FLAC; case Codec.MP1: case Codec.MP2: - return Quality.Unknown; case Codec.MP3VBR: - return Quality.MP3_VBR; case Codec.MP3CBR: - if (bitrate == 8) - { - return Quality.MP3_008; - } - - if (bitrate == 16) - { - return Quality.MP3_016; - } - - if (bitrate == 24) - { - return Quality.MP3_024; - } - - if (bitrate == 32) - { - return Quality.MP3_032; - } - - if (bitrate == 40) - { - return Quality.MP3_040; - } - - if (bitrate == 48) - { - return Quality.MP3_048; - } - - if (bitrate == 56) - { - return Quality.MP3_056; - } - - if (bitrate == 64) - { - return Quality.MP3_064; - } - - if (bitrate == 80) - { - return Quality.MP3_080; - } - - if (bitrate == 96) - { - return Quality.MP3_096; - } - - if (bitrate == 112) - { - return Quality.MP3_112; - } - - if (bitrate == 128) - { - return Quality.MP3_128; - } - - if (bitrate == 160) - { - return Quality.MP3_160; - } - - if (bitrate == 192) - { - return Quality.MP3_192; - } - - if (bitrate == 224) - { - return Quality.MP3_224; - } - - if (bitrate == 256) - { - return Quality.MP3_256; - } - - if (bitrate == 320) - { - return Quality.MP3_320; - } - - return Quality.Unknown; - case Codec.FLAC: - if (sampleSize == 24) - { - return Quality.FLAC_24; - } - - return Quality.FLAC; - case Codec.ALAC: - return Quality.ALAC; - case Codec.WAVPACK: - return Quality.WAVPACK; case Codec.APE: - return Quality.APE; case Codec.WMA: - return Quality.WMA; - case Codec.WAV: - return Quality.WAV; case Codec.AAC: - if (bitrate == 192) - { - return Quality.AAC_192; - } - - if (bitrate == 256) - { - return Quality.AAC_256; - } - - if (bitrate == 320) - { - return Quality.AAC_320; - } - - return Quality.AAC_VBR; case Codec.OGG: - if (bitrate == 160) - { - return Quality.VORBIS_Q5; - } - - if (bitrate == 192) - { - return Quality.VORBIS_Q6; - } - - if (bitrate == 224) - { - return Quality.VORBIS_Q7; - } - - if (bitrate == 256) - { - return Quality.VORBIS_Q8; - } - - if (bitrate == 320) - { - return Quality.VORBIS_Q9; - } - - if (bitrate == 500) - { - return Quality.VORBIS_Q10; - } - - return Quality.Unknown; case Codec.OPUS: - if (bitrate < 130) - { - return Quality.Unknown; - } - - if (bitrate < 180) - { - return Quality.VORBIS_Q5; - } - - if (bitrate < 205) - { - return Quality.VORBIS_Q6; - } - - if (bitrate < 240) - { - return Quality.VORBIS_Q7; - } - - if (bitrate < 290) - { - return Quality.VORBIS_Q8; - } - - if (bitrate < 410) - { - return Quality.VORBIS_Q9; - } - - return Quality.VORBIS_Q10; default: - return Quality.Unknown; + return Quality.MP3_320; } } @@ -661,28 +282,10 @@ namespace NzbDrone.Core.Parser OGG, OPUS, WAV, - Unknown - } - - public enum BitRate - { - B096, - B128, - B160, - B192, - B224, - B256, - B320, - B500, - VBR, - VBRV0, - VBRV2, - Unknown, - } - - public enum SampleSize - { - S24, + PDF, + EPUB, + MOBI, + AZW3, Unknown } } diff --git a/src/NzbDrone.Core/Profiles/Metadata/MetadataProfile.cs b/src/NzbDrone.Core/Profiles/Metadata/MetadataProfile.cs index 40dee15fe..d34f6dbc7 100644 --- a/src/NzbDrone.Core/Profiles/Metadata/MetadataProfile.cs +++ b/src/NzbDrone.Core/Profiles/Metadata/MetadataProfile.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Profiles.Metadata @@ -6,8 +5,12 @@ namespace NzbDrone.Core.Profiles.Metadata public class MetadataProfile : ModelBase { public string Name { get; set; } - public List<ProfilePrimaryAlbumTypeItem> PrimaryAlbumTypes { get; set; } - public List<ProfileSecondaryAlbumTypeItem> SecondaryAlbumTypes { get; set; } - public List<ProfileReleaseStatusItem> ReleaseStatuses { get; set; } + public double MinRating { get; set; } + public int MinRatingCount { get; set; } + public bool SkipMissingDate { get; set; } + public bool SkipMissingIsbn { get; set; } + public bool SkipPartsAndSets { get; set; } + public bool SkipSeriesSecondary { get; set; } + public string AllowedLanguages { get; set; } } } diff --git a/src/NzbDrone.Core/Profiles/Metadata/MetadataProfileService.cs b/src/NzbDrone.Core/Profiles/Metadata/MetadataProfileService.cs index aa3c31446..6730c9e06 100644 --- a/src/NzbDrone.Core/Profiles/Metadata/MetadataProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Metadata/MetadataProfileService.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.ImportLists; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; @@ -18,11 +20,15 @@ namespace NzbDrone.Core.Profiles.Metadata List<MetadataProfile> All(); MetadataProfile Get(int id); bool Exists(int id); + List<Book> FilterBooks(Author input, int profileId); } public class MetadataProfileService : IMetadataProfileService, IHandle<ApplicationStartedEvent> { public const string NONE_PROFILE_NAME = "None"; + + private static readonly Regex PartOrSetRegex = new Regex(@"(?:\d+ of \d+|\d+/\d+|(?<from>\d+)-(?<to>\d+))"); + private readonly IMetadataProfileRepository _profileRepository; private readonly IArtistService _artistService; private readonly IImportListFactory _importListFactory; @@ -87,32 +93,71 @@ namespace NzbDrone.Core.Profiles.Metadata return _profileRepository.Exists(id); } - private void AddDefaultProfile(string name, List<PrimaryAlbumType> primAllowed, List<SecondaryAlbumType> secAllowed, List<ReleaseStatus> relAllowed) + public List<Book> FilterBooks(Author input, int profileId) + { + var seriesLinks = input.Series.Value.SelectMany(x => x.LinkItems.Value) + .GroupBy(x => x.Book.Value) + .ToDictionary(x => x.Key, y => y.ToList()); + + return FilterBooks(input.Books.Value, seriesLinks, profileId); + } + + private List<Book> FilterBooks(IEnumerable<Book> books, Dictionary<Book, List<SeriesBookLink>> seriesLinks, int metadataProfileId) + { + var profile = Get(metadataProfileId); + var allowedLanguages = profile.AllowedLanguages.IsNotNullOrWhiteSpace() ? new HashSet<string>(profile.AllowedLanguages.Split(',').Select(x => x.Trim().ToLower())) : new HashSet<string>(); + + _logger.Trace($"Filtering:\n{books.Select(x => x.ToString()).Join("\n")}"); + + var hash = new HashSet<Book>(books); + var titles = new HashSet<string>(books.Select(x => x.Title)); + + FilterByPredicate(hash, profile, (x, p) => x.Ratings.Votes >= p.MinRatingCount && (double)x.Ratings.Value >= p.MinRating, "rating criteria not met"); + FilterByPredicate(hash, profile, (x, p) => !p.SkipMissingDate || x.ReleaseDate.HasValue, "release date is missing"); + FilterByPredicate(hash, profile, (x, p) => !p.SkipMissingIsbn || x.Isbn13.IsNotNullOrWhiteSpace() || x.Asin.IsNotNullOrWhiteSpace(), "isbn and asin is missing"); + FilterByPredicate(hash, profile, (x, p) => !p.SkipPartsAndSets || !IsPartOrSet(x, seriesLinks.GetValueOrDefault(x), titles), "book is part of set"); + FilterByPredicate(hash, profile, (x, p) => !p.SkipSeriesSecondary || !seriesLinks.ContainsKey(x) || seriesLinks[x].Any(y => y.IsPrimary), "book is a secondary series item"); + FilterByPredicate(hash, profile, (x, p) => !allowedLanguages.Any() || allowedLanguages.Contains(x.Language?.ToLower() ?? "null"), "book language not allowed"); + + return hash.ToList(); + } + + private void FilterByPredicate(HashSet<Book> books, MetadataProfile profile, Func<Book, MetadataProfile, bool> bookAllowed, string message) + { + var filtered = new HashSet<Book>(books.Where(x => !bookAllowed(x, profile))); + if (filtered.Any()) + { + _logger.Trace($"Skipping {filtered.Count} books because {message}:\n{filtered.ConcatToString(x => x.ToString(), "\n")}"); + books.RemoveWhere(x => filtered.Contains(x)); + } + } + + private bool IsPartOrSet(Book book, List<SeriesBookLink> seriesLinks, HashSet<string> titles) { - var primaryTypes = PrimaryAlbumType.All - .OrderByDescending(l => l.Name) - .Select(v => new ProfilePrimaryAlbumTypeItem { PrimaryAlbumType = v, Allowed = primAllowed.Contains(v) }) - .ToList(); - - var secondaryTypes = SecondaryAlbumType.All - .OrderByDescending(l => l.Name) - .Select(v => new ProfileSecondaryAlbumTypeItem { SecondaryAlbumType = v, Allowed = secAllowed.Contains(v) }) - .ToList(); - - var releaseStatues = ReleaseStatus.All - .OrderByDescending(l => l.Name) - .Select(v => new ProfileReleaseStatusItem { ReleaseStatus = v, Allowed = relAllowed.Contains(v) }) - .ToList(); - - var profile = new MetadataProfile + if (seriesLinks != null && + seriesLinks.Any(x => x.Position.IsNotNullOrWhiteSpace()) && + !seriesLinks.Any(s => double.TryParse(s.Position, out _))) + { + // No non-empty series entries parse to a number, so all like 1-3 etc. + return true; + } + + // Skip things of form Title1 / Title2 when Title1 and Title2 are already in the list + var split = book.Title.Split('/').Select(x => x.Trim()).ToList(); + if (split.Count > 1 && split.All(x => titles.Contains(x))) { - Name = name, - PrimaryAlbumTypes = primaryTypes, - SecondaryAlbumTypes = secondaryTypes, - ReleaseStatuses = releaseStatues - }; + return true; + } + + var match = PartOrSetRegex.Match(book.Title); - Add(profile); + if (match.Groups["from"].Success) + { + var from = int.Parse(match.Groups["from"].Value); + return from >= 1800 && from <= DateTime.UtcNow.Year ? false : true; + } + + return false; } public void Handle(ApplicationStartedEvent message) @@ -123,10 +168,9 @@ namespace NzbDrone.Core.Profiles.Metadata var emptyProfile = profiles.FirstOrDefault(x => x.Name == NONE_PROFILE_NAME); // make sure empty profile exists and is actually empty + // TODO: reinstate if (emptyProfile != null && - !emptyProfile.PrimaryAlbumTypes.Any(x => x.Allowed) && - !emptyProfile.SecondaryAlbumTypes.Any(x => x.Allowed) && - !emptyProfile.ReleaseStatuses.Any(x => x.Allowed)) + emptyProfile.MinRating == 100) { return; } @@ -135,7 +179,15 @@ namespace NzbDrone.Core.Profiles.Metadata { _logger.Info("Setting up standard metadata profile"); - AddDefaultProfile("Standard", new List<PrimaryAlbumType> { PrimaryAlbumType.Album }, new List<SecondaryAlbumType> { SecondaryAlbumType.Studio }, new List<ReleaseStatus> { ReleaseStatus.Official }); + Add(new MetadataProfile + { + Name = "Standard", + MinRating = 0, + MinRatingCount = 100, + SkipMissingDate = true, + SkipPartsAndSets = true, + AllowedLanguages = "eng, en-US, en-GB" + }); } if (emptyProfile != null) @@ -158,7 +210,11 @@ namespace NzbDrone.Core.Profiles.Metadata _logger.Info("Setting up empty metadata profile"); - AddDefaultProfile(NONE_PROFILE_NAME, new List<PrimaryAlbumType>(), new List<SecondaryAlbumType>(), new List<ReleaseStatus>()); + Add(new MetadataProfile + { + Name = NONE_PROFILE_NAME, + MinRating = 100 + }); } } } diff --git a/src/NzbDrone.Core/Profiles/Metadata/ProfilePrimaryAlbumTypeItem.cs b/src/NzbDrone.Core/Profiles/Metadata/ProfilePrimaryAlbumTypeItem.cs deleted file mode 100644 index 6f1c008f1..000000000 --- a/src/NzbDrone.Core/Profiles/Metadata/ProfilePrimaryAlbumTypeItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Profiles.Metadata -{ - public class ProfilePrimaryAlbumTypeItem : IEmbeddedDocument - { - public PrimaryAlbumType PrimaryAlbumType { get; set; } - public bool Allowed { get; set; } - } -} diff --git a/src/NzbDrone.Core/Profiles/Metadata/ProfileReleaseStatusTypeItem.cs b/src/NzbDrone.Core/Profiles/Metadata/ProfileReleaseStatusTypeItem.cs deleted file mode 100644 index 2475c2534..000000000 --- a/src/NzbDrone.Core/Profiles/Metadata/ProfileReleaseStatusTypeItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Profiles.Metadata -{ - public class ProfileReleaseStatusItem : IEmbeddedDocument - { - public ReleaseStatus ReleaseStatus { get; set; } - public bool Allowed { get; set; } - } -} diff --git a/src/NzbDrone.Core/Profiles/Metadata/ProfileSecondaryAlbumTypeItem.cs b/src/NzbDrone.Core/Profiles/Metadata/ProfileSecondaryAlbumTypeItem.cs deleted file mode 100644 index d9aacf570..000000000 --- a/src/NzbDrone.Core/Profiles/Metadata/ProfileSecondaryAlbumTypeItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Profiles.Metadata -{ - public class ProfileSecondaryAlbumTypeItem : IEmbeddedDocument - { - public SecondaryAlbumType SecondaryAlbumType { get; set; } - public bool Allowed { get; set; } - } -} diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs index bcceaccf4..5bee70b35 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs @@ -90,53 +90,28 @@ namespace NzbDrone.Core.Profiles.Qualities _logger.Info("Setting up default quality profiles"); AddDefaultProfile("Any", - Quality.Unknown, - Quality.Unknown, - Quality.MP3_008, - Quality.MP3_016, - Quality.MP3_024, - Quality.MP3_032, - Quality.MP3_040, - Quality.MP3_048, - Quality.MP3_056, - Quality.MP3_064, - Quality.MP3_080, - Quality.MP3_096, - Quality.MP3_112, - Quality.MP3_128, - Quality.MP3_160, - Quality.MP3_192, - Quality.MP3_224, - Quality.MP3_256, - Quality.MP3_320, - Quality.MP3_VBR, - Quality.MP3_VBR_V2, - Quality.AAC_192, - Quality.AAC_256, - Quality.AAC_320, - Quality.AAC_VBR, - Quality.VORBIS_Q5, - Quality.VORBIS_Q6, - Quality.VORBIS_Q7, - Quality.VORBIS_Q8, - Quality.VORBIS_Q9, - Quality.VORBIS_Q10, - Quality.WMA, - Quality.ALAC, - Quality.FLAC, - Quality.FLAC_24); - - AddDefaultProfile("Lossless", - Quality.FLAC, - Quality.FLAC, - Quality.ALAC, - Quality.FLAC_24); - - AddDefaultProfile("Standard", - Quality.MP3_192, - Quality.MP3_192, - Quality.MP3_256, - Quality.MP3_320); + Quality.Unknown, + Quality.Unknown, + Quality.PDF, + Quality.MOBI, + Quality.EPUB, + Quality.AZW3, + Quality.MP3_320, + Quality.FLAC); + + AddDefaultProfile("Lossless Audio", + Quality.FLAC, + Quality.FLAC); + + AddDefaultProfile("Standard Audio", + Quality.MP3_320, + Quality.MP3_320); + + AddDefaultProfile("Text", + Quality.MOBI, + Quality.MOBI, + Quality.EPUB, + Quality.AZW3); } public QualityProfile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed) diff --git a/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs b/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs index 9a60d5eb9..ff2909d56 100644 --- a/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs +++ b/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs @@ -8,8 +8,8 @@ namespace NzbDrone.Core.Profiles.Releases { public interface IPreferredWordService { - int Calculate(Artist artist, string title); - List<string> GetMatchingPreferredWords(Artist artist, string title); + int Calculate(Author artist, string title); + List<string> GetMatchingPreferredWords(Author artist, string title); } public class PreferredWordService : IPreferredWordService @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Profiles.Releases _logger = logger; } - public int Calculate(Artist series, string title) + public int Calculate(Author series, string title) { _logger.Trace("Calculating preferred word score for '{0}'", title); @@ -52,7 +52,7 @@ namespace NzbDrone.Core.Profiles.Releases return score; } - public List<string> GetMatchingPreferredWords(Artist artist, string title) + public List<string> GetMatchingPreferredWords(Author artist, string title) { var releaseProfiles = _releaseProfileService.AllForTags(artist.Tags); var matchingPairs = new List<KeyValuePair<string, int>>(); diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index 0085b0683..35c7b451e 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -71,84 +71,24 @@ namespace NzbDrone.Core.Qualities } public static Quality Unknown => new Quality(0, "Unknown"); - public static Quality MP3_192 => new Quality(1, "MP3-192"); - public static Quality MP3_VBR => new Quality(2, "MP3-VBR-V0"); - public static Quality MP3_256 => new Quality(3, "MP3-256"); - public static Quality MP3_320 => new Quality(4, "MP3-320"); - public static Quality MP3_160 => new Quality(5, "MP3-160"); - public static Quality FLAC => new Quality(6, "FLAC"); - public static Quality ALAC => new Quality(7, "ALAC"); - public static Quality MP3_VBR_V2 => new Quality(8, "MP3-VBR-V2"); - public static Quality AAC_192 => new Quality(9, "AAC-192"); - public static Quality AAC_256 => new Quality(10, "AAC-256"); - public static Quality AAC_320 => new Quality(11, "AAC-320"); - public static Quality AAC_VBR => new Quality(12, "AAC-VBR"); - public static Quality WAV => new Quality(13, "WAV"); - public static Quality VORBIS_Q10 => new Quality(14, "OGG Vorbis Q10"); - public static Quality VORBIS_Q9 => new Quality(15, "OGG Vorbis Q9"); - public static Quality VORBIS_Q8 => new Quality(16, "OGG Vorbis Q8"); - public static Quality VORBIS_Q7 => new Quality(17, "OGG Vorbis Q7"); - public static Quality VORBIS_Q6 => new Quality(18, "OGG Vorbis Q6"); - public static Quality VORBIS_Q5 => new Quality(19, "OGG Vorbis Q5"); - public static Quality WMA => new Quality(20, "WMA"); - public static Quality FLAC_24 => new Quality(21, "FLAC 24bit"); - public static Quality MP3_128 => new Quality(22, "MP3-128"); - public static Quality MP3_096 => new Quality(23, "MP3-96"); // For Current Files Only - public static Quality MP3_080 => new Quality(24, "MP3-80"); // For Current Files Only - public static Quality MP3_064 => new Quality(25, "MP3-64"); // For Current Files Only - public static Quality MP3_056 => new Quality(26, "MP3-56"); // For Current Files Only - public static Quality MP3_048 => new Quality(27, "MP3-48"); // For Current Files Only - public static Quality MP3_040 => new Quality(28, "MP3-40"); // For Current Files Only - public static Quality MP3_032 => new Quality(29, "MP3-32"); // For Current Files Only - public static Quality MP3_024 => new Quality(30, "MP3-24"); // For Current Files Only - public static Quality MP3_016 => new Quality(31, "MP3-16"); // For Current Files Only - public static Quality MP3_008 => new Quality(32, "MP3-8"); // For Current Files Only - public static Quality MP3_112 => new Quality(33, "MP3-112"); // For Current Files Only - public static Quality MP3_224 => new Quality(34, "MP3-224"); // For Current Files Only - public static Quality APE => new Quality(35, "APE"); - public static Quality WAVPACK => new Quality(36, "WavPack"); + public static Quality PDF => new Quality(1, "PDF"); + public static Quality MOBI => new Quality(2, "MOBI"); + public static Quality EPUB => new Quality(3, "EPUB"); + public static Quality AZW3 => new Quality(4, "AZW3"); + public static Quality MP3_320 => new Quality(10, "MP3-320"); + public static Quality FLAC => new Quality(11, "FLAC"); static Quality() { All = new List<Quality> { Unknown, - MP3_008, - MP3_016, - MP3_024, - MP3_032, - MP3_040, - MP3_048, - MP3_056, - MP3_064, - MP3_080, - MP3_096, - MP3_112, - MP3_128, - MP3_160, - MP3_192, - MP3_224, - MP3_VBR, - MP3_256, + PDF, + MOBI, + EPUB, + AZW3, MP3_320, - MP3_VBR_V2, - AAC_192, - AAC_256, - AAC_320, - AAC_VBR, - WMA, - VORBIS_Q10, - VORBIS_Q9, - VORBIS_Q8, - VORBIS_Q7, - VORBIS_Q6, - VORBIS_Q5, - ALAC, - FLAC, - APE, - WAVPACK, - FLAC_24, - WAV + FLAC }; AllLookup = new Quality[All.Select(v => v.Id).Max() + 1]; @@ -160,42 +100,12 @@ namespace NzbDrone.Core.Qualities DefaultQualityDefinitions = new HashSet<QualityDefinition> { new QualityDefinition(Quality.Unknown) { Weight = 1, MinSize = 0, MaxSize = 350, GroupWeight = 1 }, - new QualityDefinition(Quality.MP3_008) { Weight = 2, MinSize = 0, MaxSize = 10, GroupName = "Trash Quality Lossy", GroupWeight = 2 }, - new QualityDefinition(Quality.MP3_016) { Weight = 3, MinSize = 0, MaxSize = 20, GroupName = "Trash Quality Lossy", GroupWeight = 2 }, - new QualityDefinition(Quality.MP3_024) { Weight = 4, MinSize = 0, MaxSize = 30, GroupName = "Trash Quality Lossy", GroupWeight = 2 }, - new QualityDefinition(Quality.MP3_032) { Weight = 5, MinSize = 0, MaxSize = 40, GroupName = "Trash Quality Lossy", GroupWeight = 2 }, - new QualityDefinition(Quality.MP3_040) { Weight = 6, MinSize = 0, MaxSize = 45, GroupName = "Trash Quality Lossy", GroupWeight = 2 }, - new QualityDefinition(Quality.MP3_048) { Weight = 7, MinSize = 0, MaxSize = 55, GroupName = "Trash Quality Lossy", GroupWeight = 2 }, - new QualityDefinition(Quality.MP3_056) { Weight = 8, MinSize = 0, MaxSize = 65, GroupName = "Trash Quality Lossy", GroupWeight = 2 }, - new QualityDefinition(Quality.MP3_064) { Weight = 9, MinSize = 0, MaxSize = 75, GroupName = "Trash Quality Lossy", GroupWeight = 2 }, - new QualityDefinition(Quality.MP3_080) { Weight = 10, MinSize = 0, MaxSize = 95, GroupName = "Trash Quality Lossy", GroupWeight = 2 }, - new QualityDefinition(Quality.MP3_096) { Weight = 11, MinSize = 0, MaxSize = 110, GroupName = "Poor Quality Lossy", GroupWeight = 3 }, - new QualityDefinition(Quality.MP3_112) { Weight = 12, MinSize = 0, MaxSize = 125, GroupName = "Poor Quality Lossy", GroupWeight = 3 }, - new QualityDefinition(Quality.MP3_128) { Weight = 13, MinSize = 0, MaxSize = 140, GroupName = "Poor Quality Lossy", GroupWeight = 3 }, - new QualityDefinition(Quality.VORBIS_Q5) { Weight = 14, MinSize = 0, MaxSize = 175, GroupName = "Poor Quality Lossy", GroupWeight = 3 }, - new QualityDefinition(Quality.MP3_160) { Weight = 14, MinSize = 0, MaxSize = 175, GroupName = "Poor Quality Lossy", GroupWeight = 3 }, - new QualityDefinition(Quality.MP3_192) { Weight = 15, MinSize = 0, MaxSize = 210, GroupName = "Low Quality Lossy", GroupWeight = 4 }, - new QualityDefinition(Quality.VORBIS_Q6) { Weight = 15, MinSize = 0, MaxSize = 210, GroupName = "Low Quality Lossy", GroupWeight = 4 }, - new QualityDefinition(Quality.AAC_192) { Weight = 15, MinSize = 0, MaxSize = 210, GroupName = "Low Quality Lossy", GroupWeight = 4 }, - new QualityDefinition(Quality.WMA) { Weight = 15, MinSize = 0, MaxSize = 350, GroupName = "Low Quality Lossy", GroupWeight = 4 }, - new QualityDefinition(Quality.MP3_224) { Weight = 16, MinSize = 0, MaxSize = 245, GroupName = "Low Quality Lossy", GroupWeight = 4 }, - new QualityDefinition(Quality.VORBIS_Q7) { Weight = 17, MinSize = 0, MaxSize = 245, GroupName = "Mid Quality Lossy", GroupWeight = 5 }, - new QualityDefinition(Quality.MP3_VBR_V2) { Weight = 18, MinSize = 0, MaxSize = 280, GroupName = "Mid Quality Lossy", GroupWeight = 5 }, - new QualityDefinition(Quality.MP3_256) { Weight = 18, MinSize = 0, MaxSize = 280, GroupName = "Mid Quality Lossy", GroupWeight = 5 }, - new QualityDefinition(Quality.VORBIS_Q8) { Weight = 18, MinSize = 0, MaxSize = 280, GroupName = "Mid Quality Lossy", GroupWeight = 5 }, - new QualityDefinition(Quality.AAC_256) { Weight = 18, MinSize = 0, MaxSize = 280, GroupName = "Mid Quality Lossy", GroupWeight = 5 }, - new QualityDefinition(Quality.MP3_VBR) { Weight = 19, MinSize = 0, MaxSize = 350, GroupName = "High Quality Lossy", GroupWeight = 6 }, - new QualityDefinition(Quality.AAC_VBR) { Weight = 19, MinSize = 0, MaxSize = 350, GroupName = "High Quality Lossy", GroupWeight = 6 }, - new QualityDefinition(Quality.MP3_320) { Weight = 20, MinSize = 0, MaxSize = 350, GroupName = "High Quality Lossy", GroupWeight = 6 }, - new QualityDefinition(Quality.VORBIS_Q9) { Weight = 20, MinSize = 0, MaxSize = 350, GroupName = "High Quality Lossy", GroupWeight = 6 }, - new QualityDefinition(Quality.AAC_320) { Weight = 20, MinSize = 0, MaxSize = 350, GroupName = "High Quality Lossy", GroupWeight = 6 }, - new QualityDefinition(Quality.VORBIS_Q10) { Weight = 21, MinSize = 0, MaxSize = 550, GroupName = "High Quality Lossy", GroupWeight = 6 }, - new QualityDefinition(Quality.ALAC) { Weight = 22, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, - new QualityDefinition(Quality.FLAC) { Weight = 22, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, - new QualityDefinition(Quality.APE) { Weight = 22, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, - new QualityDefinition(Quality.WAVPACK) { Weight = 22, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, - new QualityDefinition(Quality.FLAC_24) { Weight = 23, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, - new QualityDefinition(Quality.WAV) { Weight = 24, MinSize = 0, MaxSize = null, GroupWeight = 8 } + new QualityDefinition(Quality.PDF) { Weight = 5, MinSize = 0, MaxSize = 350, GroupWeight = 2 }, + new QualityDefinition(Quality.MOBI) { Weight = 10, MinSize = 0, MaxSize = 350, GroupWeight = 10 }, + new QualityDefinition(Quality.EPUB) { Weight = 11, MinSize = 0, MaxSize = 350, GroupWeight = 11 }, + new QualityDefinition(Quality.AZW3) { Weight = 12, MinSize = 0, MaxSize = 350, GroupWeight = 12 }, + new QualityDefinition(Quality.MP3_320) { Weight = 100, MinSize = 0, MaxSize = 350, GroupWeight = 100 }, + new QualityDefinition(Quality.FLAC) { Weight = 110, MinSize = 0, MaxSize = null, GroupWeight = 110 }, }; } diff --git a/src/NzbDrone.Core/Qualities/Revision.cs b/src/NzbDrone.Core/Qualities/Revision.cs index 42c413c37..1eccec25f 100644 --- a/src/NzbDrone.Core/Qualities/Revision.cs +++ b/src/NzbDrone.Core/Qualities/Revision.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Core.Qualities { public class Revision : IEquatable<Revision>, IComparable<Revision> { - private Revision() + public Revision() { } diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index e8e994b37..a509ae210 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -11,8 +11,8 @@ namespace NzbDrone.Core.Queue { public class Queue : ModelBase { - public Artist Artist { get; set; } - public Album Album { get; set; } + public Author Artist { get; set; } + public Book Album { get; set; } public QualityModel Quality { get; set; } public decimal Size { get; set; } public string Title { get; set; } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 9783c0fc0..bdec36a37 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Queue } } - private Queue MapQueueItem(TrackedDownload trackedDownload, Album album) + private Queue MapQueueItem(TrackedDownload trackedDownload, Book album) { bool downloadForced = false; var history = _historyService.Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed).FirstOrDefault(); diff --git a/src/NzbDrone.Core/Readarr.Core.csproj b/src/NzbDrone.Core/Readarr.Core.csproj index 7b8fc4f6b..d8fbccb37 100644 --- a/src/NzbDrone.Core/Readarr.Core.csproj +++ b/src/NzbDrone.Core/Readarr.Core.csproj @@ -5,6 +5,7 @@ <ItemGroup> <PackageReference Include="Dapper" Version="2.0.30" /> <PackageReference Include="System.Text.Json" Version="4.7.0" /> + <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" /> <PackageReference Include="System.Memory" Version="4.5.3" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.0" /> @@ -17,10 +18,10 @@ <PackageReference Include="System.IO.Abstractions" Version="7.0.15" /> <PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.19" /> <PackageReference Include="Kveer.XmlRPC" Version="1.1.1" /> - <PackageReference Include="SpotifyAPI.Web" Version="4.2.2" /> <PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0007" /> <PackageReference Include="Equ" Version="2.2.0" /> <PackageReference Include="MonoTorrent" Version="1.0.11" /> + <PackageReference Include="PdfSharpCore" Version="1.1.25" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Readarr.Common.csproj" /> diff --git a/src/NzbDrone.Core/RootFolders/RootFolder.cs b/src/NzbDrone.Core/RootFolders/RootFolder.cs index 528d02cd8..5daaa2451 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolder.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolder.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using NzbDrone.Core.Books.Calibre; using NzbDrone.Core.Datastore; using NzbDrone.Core.Music; @@ -12,6 +13,8 @@ namespace NzbDrone.Core.RootFolders public int DefaultQualityProfileId { get; set; } public MonitorTypes DefaultMonitorOption { get; set; } public HashSet<int> DefaultTags { get; set; } + public bool IsCalibreLibrary { get; set; } + public CalibreSettings CalibreSettings { get; set; } public bool Accessible { get; set; } public long? FreeSpace { get; set; } diff --git a/src/NzbDrone.Core/RootFolders/RootFolderRepository.cs b/src/NzbDrone.Core/RootFolders/RootFolderRepository.cs index 47cc06d55..bac996311 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderRepository.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderRepository.cs @@ -1,4 +1,9 @@ -using NzbDrone.Core.Datastore; +using System.Collections.Generic; +using System.Text.Json; +using Dapper; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Books.Calibre; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.RootFolders @@ -16,6 +21,37 @@ namespace NzbDrone.Core.RootFolders protected override bool PublishModelEvents => true; + protected override List<RootFolder> Query(SqlBuilder builder) + { + var type = typeof(RootFolder); + var sql = builder.Select(type).AddSelectTemplate(type); + + var results = new List<RootFolder>(); + + using (var conn = _database.OpenConnection()) + using (var reader = conn.ExecuteReader(sql.RawSql, sql.Parameters)) + { + var parser = reader.GetRowParser<RootFolder>(type); + var settingsIndex = reader.GetOrdinal(nameof(RootFolder.CalibreSettings)); + var serializerSettings = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + + while (reader.Read()) + { + var body = reader.IsDBNull(settingsIndex) ? null : reader.GetString(settingsIndex); + var item = parser(reader); + + if (body.IsNotNullOrWhiteSpace()) + { + item.CalibreSettings = JsonSerializer.Deserialize<CalibreSettings>(body, serializerSettings); + } + + results.Add(item); + } + } + + return results; + } + public new void Delete(int id) { var model = Get(id); diff --git a/src/NzbDrone.Core/Tags/TagDetails.cs b/src/NzbDrone.Core/Tags/TagDetails.cs index 1730ce760..dd4ac1e5c 100644 --- a/src/NzbDrone.Core/Tags/TagDetails.cs +++ b/src/NzbDrone.Core/Tags/TagDetails.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Tags public class TagDetails : ModelBase { public string Label { get; set; } - public List<int> ArtistIds { get; set; } + public List<int> AuthorIds { get; set; } public List<int> NotificationIds { get; set; } public List<int> RestrictionIds { get; set; } public List<int> DelayProfileIds { get; set; } @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Tags { get { - return ArtistIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || RootFolderIds.Any(); + return AuthorIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || RootFolderIds.Any(); } } } diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index 9c5d07eb6..37a4f2a92 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Tags ImportListIds = importLists.Select(c => c.Id).ToList(), NotificationIds = notifications.Select(c => c.Id).ToList(), RestrictionIds = restrictions.Select(c => c.Id).ToList(), - ArtistIds = artist.Select(c => c.Id).ToList(), + AuthorIds = artist.Select(c => c.Id).ToList(), RootFolderIds = rootFolders.Select(c => c.Id).ToList() }; } @@ -115,7 +115,7 @@ namespace NzbDrone.Core.Tags ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), - ArtistIds = artists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), + AuthorIds = artists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), RootFolderIds = rootFolders.Where(c => c.DefaultTags.Contains(tag.Id)).Select(c => c.Id).ToList() }); } diff --git a/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs index e17a8ab16..7812f5ddc 100644 --- a/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Validation.Paths return true; } - return !_artistService.GetAllArtists().Exists(s => s.Metadata.Value.ForeignArtistId == context.PropertyValue.ToString()); + return !_artistService.GetAllArtists().Exists(s => s.Metadata.Value.ForeignAuthorId == context.PropertyValue.ToString()); } } } diff --git a/src/NzbDrone.Host/NzbDrone.ico b/src/NzbDrone.Host/NzbDrone.ico deleted file mode 100644 index ef74f8e5f4aa23bea5653f9de29923930617bc5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370070 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�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 diff --git a/src/NzbDrone.Host/Readarr.ico b/src/NzbDrone.Host/Readarr.ico new file mode 100644 index 0000000000000000000000000000000000000000..516e3108892931fc9af8c0fbe8332c07fa2faf5f GIT binary patch literal 324065 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 literal 0 HcmV?d00001 diff --git a/src/NzbDrone.Integration.Test/ApiTests/ArtistEditorFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ArtistEditorFixture.cs index 77b6b7e53..e9c8772f3 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ArtistEditorFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ArtistEditorFixture.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Integration.Test.ApiTests var artistEditor = new ArtistEditorResource { QualityProfileId = 2, - ArtistIds = artist.Select(o => o.Id).ToList() + AuthorIds = artist.Select(o => o.Id).ToList() }; var result = Artist.Editor(artistEditor); diff --git a/src/NzbDrone.Integration.Test/ApiTests/ArtistFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ArtistFixture.cs index 9252434db..0f443e514 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ArtistFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ArtistFixture.cs @@ -13,10 +13,10 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(0)] public void add_artist_with_tags_should_store_them() { - EnsureNoArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park"); + EnsureNoArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "J.K. Rowling"); var tag = EnsureTag("abc"); - var artist = Artist.Lookup("readarr:f59c5520-5f46-4d2c-b2c4-822eabf53419").Single(); + var artist = Artist.Lookup("readarr:1").Single(); artist.QualityProfileId = 1; artist.MetadataProfileId = 1; @@ -36,9 +36,9 @@ namespace NzbDrone.Integration.Test.ApiTests { IgnoreOnMonoVersions("5.12", "5.14"); - EnsureNoArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park"); + EnsureNoArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "J.K. Rowling"); - var artist = Artist.Lookup("readarr:f59c5520-5f46-4d2c-b2c4-822eabf53419").Single(); + var artist = Artist.Lookup("readarr:1").Single(); artist.Path = Path.Combine(ArtistRootFolder, artist.ArtistName); @@ -51,9 +51,9 @@ namespace NzbDrone.Integration.Test.ApiTests { IgnoreOnMonoVersions("5.12", "5.14"); - EnsureNoArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park"); + EnsureNoArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "J.K. Rowling"); - var artist = Artist.Lookup("readarr:f59c5520-5f46-4d2c-b2c4-822eabf53419").Single(); + var artist = Artist.Lookup("readarr:1").Single(); artist.QualityProfileId = 1; @@ -64,9 +64,9 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(1)] public void add_artist() { - EnsureNoArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park"); + EnsureNoArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "J.K. Rowling"); - var artist = Artist.Lookup("readarr:f59c5520-5f46-4d2c-b2c4-822eabf53419").Single(); + var artist = Artist.Lookup("readarr:1").Single(); artist.QualityProfileId = 1; artist.MetadataProfileId = 1; @@ -85,25 +85,25 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(2)] public void get_all_artist() { - EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); - EnsureArtist("cc197bad-dc9c-440d-a5b5-d52ba2e14234", "Coldplay"); + EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling"); + EnsureArtist("amzn1.gr.author.v1.qTrNu9-PIaaBj5gYRDmN4Q", "34497", "Terry Pratchett"); var artists = Artist.All(); artists.Should().NotBeNullOrEmpty(); - artists.Should().Contain(v => v.ForeignArtistId == "8ac6cc32-8ddf-43b1-9ac4-4b04f9053176"); - artists.Should().Contain(v => v.ForeignArtistId == "cc197bad-dc9c-440d-a5b5-d52ba2e14234"); + artists.Should().Contain(v => v.ForeignAuthorId == "amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ"); + artists.Should().Contain(v => v.ForeignAuthorId == "amzn1.gr.author.v1.qTrNu9-PIaaBj5gYRDmN4Q"); } [Test] [Order(2)] public void get_artist_by_id() { - var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling"); var result = Artist.Get(artist.Id); - result.ForeignArtistId.Should().Be("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176"); + result.ForeignAuthorId.Should().Be("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ"); } [Test] @@ -118,7 +118,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(2)] public void update_artist_profile_id() { - var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling"); var profileId = 1; if (artist.QualityProfileId == profileId) @@ -137,7 +137,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(3)] public void update_artist_monitored() { - var artist = EnsureArtist("f59c5520-5f46-4d2c-b2c4-822eabf53419", "Linkin Park", false); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", false); artist.Monitored.Should().BeFalse(); @@ -159,7 +159,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(3)] public void update_artist_tags() { - var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling"); var tag = EnsureTag("abc"); if (artist.Tags.Contains(tag.Id)) @@ -182,13 +182,13 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(4)] public void delete_artist() { - var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling"); Artist.Get(artist.Id).Should().NotBeNull(); Artist.Delete(artist.Id); - Artist.All().Should().NotContain(v => v.ForeignArtistId == "8ac6cc32-8ddf-43b1-9ac4-4b04f9053176"); + Artist.All().Should().NotContain(v => v.ForeignAuthorId == "amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ"); } } } diff --git a/src/NzbDrone.Integration.Test/ApiTests/ArtistLookupFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ArtistLookupFixture.cs index c994f6aea..16954aa9c 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ArtistLookupFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ArtistLookupFixture.cs @@ -6,8 +6,8 @@ namespace NzbDrone.Integration.Test.ApiTests [TestFixture] public class ArtistLookupFixture : IntegrationTest { - [TestCase("Kiss", "Kiss")] - [TestCase("Linkin Park", "Linkin Park")] + [TestCase("Robert Harris", "Robert Harris")] + [TestCase("J.K. Rowling", "J.K. Rowling")] public void lookup_new_artist_by_name(string term, string name) { var artist = Artist.Lookup(term); @@ -17,21 +17,12 @@ namespace NzbDrone.Integration.Test.ApiTests } [Test] - public void lookup_new_artist_by_mbid() + public void lookup_new_artist_by_goodreads_book_id() { - var artist = Artist.Lookup("readarr:f59c5520-5f46-4d2c-b2c4-822eabf53419"); - - artist.Should().NotBeEmpty(); - artist.Should().Contain(c => c.ArtistName == "Linkin Park"); - } - - [Test] - [Ignore("Unreliable")] - public void lookup_random_artist_using_asterix() - { - var artist = Artist.Lookup("*"); + var artist = Artist.Lookup("readarr:1"); artist.Should().NotBeEmpty(); + artist.Should().Contain(c => c.ArtistName == "J.K. Rowling"); } } } diff --git a/src/NzbDrone.Integration.Test/ApiTests/BlacklistFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/BlacklistFixture.cs index 7ec33d1df..f5777761b 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/BlacklistFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/BlacklistFixture.cs @@ -14,11 +14,11 @@ namespace NzbDrone.Integration.Test.ApiTests [Ignore("Adding to blacklist not supported")] public void should_be_able_to_add_to_blacklist() { - _artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); + _artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling"); Blacklist.Post(new BlacklistResource { - ArtistId = _artist.Id, + AuthorId = _artist.Id, SourceTitle = "Blacklist - Album 1 [2015 FLAC]" }); } diff --git a/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs index 3b45b2c9c..c877c37dd 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/CalendarFixture.cs @@ -23,31 +23,31 @@ namespace NzbDrone.Integration.Test.ApiTests [Test] public void should_be_able_to_get_albums() { - var artist = EnsureArtist("cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493", "Adele", true); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", true); var request = Calendar.BuildRequest(); - request.AddParameter("start", new DateTime(2015, 11, 19).ToString("s") + "Z"); - request.AddParameter("end", new DateTime(2015, 11, 21).ToString("s") + "Z"); + request.AddParameter("start", new DateTime(2003, 06, 20).ToString("s") + "Z"); + request.AddParameter("end", new DateTime(2003, 06, 22).ToString("s") + "Z"); var items = Calendar.Get<List<AlbumResource>>(request); - items = items.Where(v => v.ArtistId == artist.Id).ToList(); + items = items.Where(v => v.AuthorId == artist.Id).ToList(); items.Should().HaveCount(1); - items.First().Title.Should().Be("25"); + items.First().Title.Should().Be("Harry Potter and the Order of the Phoenix"); } [Test] public void should_not_be_able_to_get_unmonitored_albums() { - var artist = EnsureArtist("cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493", "Adele", false); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", false); var request = Calendar.BuildRequest(); - request.AddParameter("start", new DateTime(2015, 11, 19).ToString("s") + "Z"); - request.AddParameter("end", new DateTime(2015, 11, 21).ToString("s") + "Z"); + request.AddParameter("start", new DateTime(2003, 06, 20).ToString("s") + "Z"); + request.AddParameter("end", new DateTime(2003, 06, 22).ToString("s") + "Z"); request.AddParameter("unmonitored", "false"); var items = Calendar.Get<List<AlbumResource>>(request); - items = items.Where(v => v.ArtistId == artist.Id).ToList(); + items = items.Where(v => v.AuthorId == artist.Id).ToList(); items.Should().BeEmpty(); } @@ -55,18 +55,18 @@ namespace NzbDrone.Integration.Test.ApiTests [Test] public void should_be_able_to_get_unmonitored_albums() { - var artist = EnsureArtist("cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493", "Adele", false); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", false); var request = Calendar.BuildRequest(); - request.AddParameter("start", new DateTime(2015, 11, 19).ToString("s") + "Z"); - request.AddParameter("end", new DateTime(2015, 11, 21).ToString("s") + "Z"); + request.AddParameter("start", new DateTime(2003, 06, 20).ToString("s") + "Z"); + request.AddParameter("end", new DateTime(2003, 06, 22).ToString("s") + "Z"); request.AddParameter("unmonitored", "true"); var items = Calendar.Get<List<AlbumResource>>(request); - items = items.Where(v => v.ArtistId == artist.Id).ToList(); + items = items.Where(v => v.AuthorId == artist.Id).ToList(); items.Should().HaveCount(1); - items.First().Title.Should().Be("25"); + items.First().Title.Should().Be("Harry Potter and the Order of the Phoenix"); } } } diff --git a/src/NzbDrone.Integration.Test/ApiTests/FileSystemFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/FileSystemFixture.cs index b286b4d25..1dcee1ab1 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/FileSystemFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/FileSystemFixture.cs @@ -105,7 +105,7 @@ namespace NzbDrone.Integration.Test.ApiTests public void get_all_mediafiles() { var tempDir = GetTempDirectory("mediaDir"); - File.WriteAllText(Path.Combine(tempDir, "somevideo.mp3"), "audio"); + File.WriteAllText(Path.Combine(tempDir, "somevideo.mobi"), "audio"); var request = FileSystem.BuildRequest("mediafiles"); request.Method = Method.GET; @@ -117,7 +117,7 @@ namespace NzbDrone.Integration.Test.ApiTests result.First().Should().ContainKey("path"); result.First().Should().ContainKey("name"); - result.First()["name"].Should().Be("somevideo.mp3"); + result.First()["name"].Should().Be("somevideo.mobi"); } } } diff --git a/src/NzbDrone.Integration.Test/ApiTests/NotificationFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/NotificationFixture.cs index 28894e732..f07ed49eb 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/NotificationFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/NotificationFixture.cs @@ -30,10 +30,10 @@ namespace NzbDrone.Integration.Test.ApiTests { var schema = Notifications.Schema(); - var xbmc = schema.Single(s => s.Implementation.Equals("Xbmc", StringComparison.InvariantCultureIgnoreCase)); + var xbmc = schema.Single(s => s.Implementation.Equals("Webhook", StringComparison.InvariantCultureIgnoreCase)); - xbmc.Name = "Test XBMC"; - xbmc.Fields.Single(f => f.Name.Equals("host")).Value = "localhost"; + xbmc.Name = "Test Webhook"; + xbmc.Fields.Single(f => f.Name.Equals("url")).Value = "http://httpbin.org/post"; var result = Notifications.Post(xbmc); Notifications.Delete(result.Id); diff --git a/src/NzbDrone.Integration.Test/ApiTests/ReleasePushFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ReleasePushFixture.cs index 01b783f65..cd6b06e28 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ReleasePushFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ReleasePushFixture.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Integration.Test.ApiTests var body = new Dictionary<string, object>(); body.Add("title", "The Artist - The Album (2008) [FLAC]"); body.Add("protocol", "Torrent"); - body.Add("downloadUrl", "https://readarr.audio/test.torrent"); + body.Add("downloadUrl", "https://readarr.com/test.torrent"); body.Add("publishDate", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ssZ", CultureInfo.InvariantCulture)); var request = ReleasePush.BuildRequest(); diff --git a/src/NzbDrone.Integration.Test/ApiTests/TrackFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/TrackFixture.cs deleted file mode 100644 index c3eaecf04..000000000 --- a/src/NzbDrone.Integration.Test/ApiTests/TrackFixture.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Linq; -using System.Threading; -using FluentAssertions; -using NUnit.Framework; -using Readarr.Api.V1.Artist; - -namespace NzbDrone.Integration.Test.ApiTests -{ - [TestFixture] - public class TrackFixture : IntegrationTest - { - private ArtistResource _artist; - - [SetUp] - public void Setup() - { - _artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); - } - - [Test] - [Order(0)] - public void should_be_able_to_get_all_tracks_in_artist() - { - Tracks.GetTracksInArtist(_artist.Id).Count.Should().BeGreaterThan(0); - } - - [Test] - [Order(1)] - public void should_be_able_to_get_a_single_track() - { - var tracks = Tracks.GetTracksInArtist(_artist.Id); - - Tracks.Get(tracks.First().Id).Should().NotBeNull(); - } - - [TearDown] - public void TearDown() - { - Artist.Delete(_artist.Id); - Thread.Sleep(2000); - } - } -} diff --git a/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs index 0cf1d4dae..7f0dce79c 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(0)] public void missing_should_be_empty() { - EnsureNoArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm"); + EnsureNoArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "J.K. Rowling"); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc"); @@ -39,7 +39,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(1)] public void missing_should_have_monitored_items() { - EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", true); + EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", true); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc"); @@ -50,21 +50,21 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(1)] public void missing_should_have_artist() { - EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", true); + EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", true); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc"); result.Records.First().Artist.Should().NotBeNull(); - result.Records.First().Artist.ArtistName.Should().Be("Alien Ant Farm"); + result.Records.First().Artist.ArtistName.Should().Be("J.K. Rowling"); } [Test] [Order(1)] public void cutoff_should_have_monitored_items() { - EnsureProfileCutoff(1, "Lossless"); - var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", true); - EnsureTrackFile(artist, 1, 1, 1, Quality.MP3_192); + EnsureProfileCutoff(1, Quality.AZW3); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", true); + EnsureTrackFile(artist, 1, Quality.MOBI); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc"); @@ -75,7 +75,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(1)] public void missing_should_not_have_unmonitored_items() { - EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", false); + EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", false); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc"); @@ -86,9 +86,9 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(1)] public void cutoff_should_not_have_unmonitored_items() { - EnsureProfileCutoff(1, "Lossless"); - var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", false); - EnsureTrackFile(artist, 1, 1, 1, Quality.MP3_192); + EnsureProfileCutoff(1, Quality.AZW3); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", false); + EnsureTrackFile(artist, 1, Quality.MOBI); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc"); @@ -99,21 +99,21 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(1)] public void cutoff_should_have_artist() { - EnsureProfileCutoff(1, "Lossless"); - var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", true); - EnsureTrackFile(artist, 1, 1, 1, Quality.MP3_192); + EnsureProfileCutoff(1, Quality.AZW3); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", true); + EnsureTrackFile(artist, 1, Quality.MOBI); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc"); result.Records.First().Artist.Should().NotBeNull(); - result.Records.First().Artist.ArtistName.Should().Be("Alien Ant Farm"); + result.Records.First().Artist.ArtistName.Should().Be("J.K. Rowling"); } [Test] [Order(2)] public void missing_should_have_unmonitored_items() { - EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", false); + EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", false); var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc", "monitored", "false"); @@ -124,9 +124,9 @@ namespace NzbDrone.Integration.Test.ApiTests [Order(2)] public void cutoff_should_have_unmonitored_items() { - EnsureProfileCutoff(1, "Lossless"); - var artist = EnsureArtist("8ac6cc32-8ddf-43b1-9ac4-4b04f9053176", "Alien Ant Farm", false); - EnsureTrackFile(artist, 1, 1, 1, Quality.MP3_192); + EnsureProfileCutoff(1, Quality.AZW3); + var artist = EnsureArtist("amzn1.gr.author.v1.SHA8asP5mFyLIP9NlujvLQ", "1", "J.K. Rowling", false); + EnsureTrackFile(artist, 1, Quality.MOBI); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc", "monitored", "false"); diff --git a/src/NzbDrone.Integration.Test/Client/AlbumClient.cs b/src/NzbDrone.Integration.Test/Client/AlbumClient.cs index 77a56312d..ebc0df11f 100644 --- a/src/NzbDrone.Integration.Test/Client/AlbumClient.cs +++ b/src/NzbDrone.Integration.Test/Client/AlbumClient.cs @@ -11,9 +11,9 @@ namespace NzbDrone.Integration.Test.Client { } - public List<AlbumResource> GetAlbumsInArtist(int artistId) + public List<AlbumResource> GetAlbumsInArtist(int authorId) { - var request = BuildRequest("?artistId=" + artistId.ToString()); + var request = BuildRequest("?authorId=" + authorId.ToString()); return Get<List<AlbumResource>>(request); } } diff --git a/src/NzbDrone.Integration.Test/Client/TrackClient.cs b/src/NzbDrone.Integration.Test/Client/TrackClient.cs deleted file mode 100644 index de336cf91..000000000 --- a/src/NzbDrone.Integration.Test/Client/TrackClient.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using Readarr.Api.V1.Tracks; -using RestSharp; - -namespace NzbDrone.Integration.Test.Client -{ - public class TrackClient : ClientBase<TrackResource> - { - public TrackClient(IRestClient restClient, string apiKey) - : base(restClient, apiKey, "track") - { - } - - public List<TrackResource> GetTracksInArtist(int artistId) - { - var request = BuildRequest("?artistId=" + artistId.ToString()); - return Get<List<TrackResource>>(request); - } - } -} diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index d4299516f..f7186842e 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -39,7 +39,6 @@ namespace NzbDrone.Integration.Test public CommandClient Commands; public DownloadClientClient DownloadClients; public AlbumClient Albums; - public TrackClient Tracks; public ClientBase<HistoryResource> History; public ClientBase<HostConfigResource> HostConfig; public IndexerClient Indexers; @@ -103,7 +102,6 @@ namespace NzbDrone.Integration.Test Commands = new CommandClient(RestClient, ApiKey); DownloadClients = new DownloadClientClient(RestClient, ApiKey); Albums = new AlbumClient(RestClient, ApiKey); - Tracks = new TrackClient(RestClient, ApiKey); History = new ClientBase<HistoryResource>(RestClient, ApiKey); HostConfig = new ClientBase<HostConfigResource>(RestClient, ApiKey, "config/host"); Indexers = new IndexerClient(RestClient, ApiKey); @@ -251,13 +249,13 @@ namespace NzbDrone.Integration.Test Assert.Fail("Timed on wait"); } - public ArtistResource EnsureArtist(string readarrId, string artistName, bool? monitored = null) + public ArtistResource EnsureArtist(string authorId, string goodreadsBookId, string artistName, bool? monitored = null) { - var result = Artist.All().FirstOrDefault(v => v.ForeignArtistId == readarrId); + var result = Artist.All().FirstOrDefault(v => v.ForeignAuthorId == authorId); if (result == null) { - var lookup = Artist.Lookup("readarr:" + readarrId); + var lookup = Artist.Lookup("readarr:" + goodreadsBookId); var artist = lookup.First(); artist.QualityProfileId = 1; artist.MetadataProfileId = 1; @@ -268,7 +266,7 @@ namespace NzbDrone.Integration.Test result = Artist.Post(artist); Commands.WaitAll(); - WaitForCompletion(() => Tracks.GetTracksInArtist(result.Id).Count > 0); + WaitForCompletion(() => Albums.GetAlbumsInArtist(result.Id).Count > 0); } var changed = false; @@ -299,7 +297,7 @@ namespace NzbDrone.Integration.Test public void EnsureNoArtist(string readarrId, string artistTitle) { - var result = Artist.All().FirstOrDefault(v => v.ForeignArtistId == readarrId); + var result = Artist.All().FirstOrDefault(v => v.ForeignAuthorId == readarrId); if (result != null) { @@ -307,11 +305,12 @@ namespace NzbDrone.Integration.Test } } - public void EnsureTrackFile(ArtistResource artist, int albumId, int albumReleaseId, int trackId, Quality quality) + public void EnsureTrackFile(ArtistResource artist, int bookId, Quality quality) { - var result = Tracks.GetTracksInArtist(artist.Id).Single(v => v.Id == trackId); + var result = Albums.GetAlbumsInArtist(artist.Id).Single(v => v.Id == bookId); - if (result.TrackFile == null) + // if (result.BookFile == null) + if (true) { var path = Path.Combine(ArtistRootFolder, artist.ArtistName, "Track.mp3"); @@ -325,30 +324,27 @@ namespace NzbDrone.Integration.Test new ManualImportFile { Path = path, - ArtistId = artist.Id, - AlbumId = albumId, - AlbumReleaseId = albumReleaseId, - TrackIds = new List<int> { trackId }, + AuthorId = artist.Id, + BookId = bookId, Quality = new QualityModel(quality) } } }); Commands.WaitAll(); - var track = Tracks.GetTracksInArtist(artist.Id).Single(x => x.Id == trackId); + var track = Albums.GetAlbumsInArtist(artist.Id).Single(x => x.Id == bookId); - track.TrackFileId.Should().NotBe(0); + // track.BookFileId.Should().NotBe(0); } } - public QualityProfileResource EnsureProfileCutoff(int profileId, string cutoff) + public QualityProfileResource EnsureProfileCutoff(int profileId, Quality cutoff) { var profile = Profiles.Get(profileId); - var cutoffItem = profile.Items.First(x => x.Name == cutoff); - if (profile.Cutoff != cutoffItem.Id) + if (profile.Cutoff != cutoff.Id) { - profile.Cutoff = cutoffItem.Id; + profile.Cutoff = cutoff.Id; profile = Profiles.Put(profile); } diff --git a/src/NzbDrone/Properties/Resources.resx b/src/NzbDrone/Properties/Resources.resx index 408bab357..51ce1ffa8 100644 --- a/src/NzbDrone/Properties/Resources.resx +++ b/src/NzbDrone/Properties/Resources.resx @@ -119,6 +119,6 @@ </resheader> <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> <data name="NzbDroneIcon" type="System.Resources.ResXFileRef, System.Windows.Forms"> - <value>..\..\NzbDrone.Host\NzbDrone.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> + <value>..\..\NzbDrone.Host\Readarr.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> </data> </root> \ No newline at end of file diff --git a/src/NzbDrone/Readarr.csproj b/src/NzbDrone/Readarr.csproj index 5330253e1..004af1e77 100644 --- a/src/NzbDrone/Readarr.csproj +++ b/src/NzbDrone/Readarr.csproj @@ -4,7 +4,7 @@ <TargetFrameworks>net462;netcoreapp3.1</TargetFrameworks> <RuntimeIdentifiers>win-x64</RuntimeIdentifiers> <UseWindowsForms>true</UseWindowsForms> - <ApplicationIcon>..\NzbDrone.Host\NzbDrone.ico</ApplicationIcon> + <ApplicationIcon>..\NzbDrone.Host\Readarr.ico</ApplicationIcon> <ApplicationManifest>app.manifest</ApplicationManifest> <GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources> </PropertyGroup> diff --git a/src/Readarr.Api.V1/Albums/AlbumLookupModule.cs b/src/Readarr.Api.V1/Albums/AlbumLookupModule.cs index acc8268dd..1ed6078cc 100644 --- a/src/Readarr.Api.V1/Albums/AlbumLookupModule.cs +++ b/src/Readarr.Api.V1/Albums/AlbumLookupModule.cs @@ -9,9 +9,9 @@ namespace Readarr.Api.V1.Albums { public class AlbumLookupModule : ReadarrRestModule<AlbumResource> { - private readonly ISearchForNewAlbum _searchProxy; + private readonly ISearchForNewBook _searchProxy; - public AlbumLookupModule(ISearchForNewAlbum searchProxy) + public AlbumLookupModule(ISearchForNewBook searchProxy) : base("/album/lookup") { _searchProxy = searchProxy; @@ -20,11 +20,11 @@ namespace Readarr.Api.V1.Albums private object Search() { - var searchResults = _searchProxy.SearchForNewAlbum((string)Request.Query.term, null); + var searchResults = _searchProxy.SearchForNewBook((string)Request.Query.term, null); return MapToResource(searchResults).ToList(); } - private static IEnumerable<AlbumResource> MapToResource(IEnumerable<NzbDrone.Core.Music.Album> albums) + private static IEnumerable<AlbumResource> MapToResource(IEnumerable<NzbDrone.Core.Music.Book> albums) { foreach (var currentAlbum in albums) { diff --git a/src/Readarr.Api.V1/Albums/AlbumModule.cs b/src/Readarr.Api.V1/Albums/AlbumModule.cs index 018f2162f..d3b9defce 100644 --- a/src/Readarr.Api.V1/Albums/AlbumModule.cs +++ b/src/Readarr.Api.V1/Albums/AlbumModule.cs @@ -30,13 +30,11 @@ namespace Readarr.Api.V1.Albums IHandle<TrackFileDeletedEvent> { protected readonly IArtistService _artistService; - protected readonly IReleaseService _releaseService; protected readonly IAddAlbumService _addAlbumService; public AlbumModule(IArtistService artistService, IAlbumService albumService, IAddAlbumService addAlbumService, - IReleaseService releaseService, IArtistStatisticsService artistStatisticsService, IMapCoversToLocal coverMapper, IUpgradableSpecification upgradableSpecification, @@ -47,7 +45,6 @@ namespace Readarr.Api.V1.Albums : base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster) { _artistService = artistService; - _releaseService = releaseService; _addAlbumService = addAlbumService; GetResourceAll = GetAlbums; @@ -56,78 +53,69 @@ namespace Readarr.Api.V1.Albums DeleteResource = DeleteAlbum; Put("/monitor", x => SetAlbumsMonitored()); - PostValidator.RuleFor(s => s.ForeignAlbumId).NotEmpty(); + PostValidator.RuleFor(s => s.ForeignBookId).NotEmpty(); PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(qualityProfileExistsValidator); PostValidator.RuleFor(s => s.Artist.MetadataProfileId).SetValidator(metadataProfileExistsValidator); PostValidator.RuleFor(s => s.Artist.RootFolderPath).IsValidPath().When(s => s.Artist.Path.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.Artist.ForeignArtistId).NotEmpty(); + PostValidator.RuleFor(s => s.Artist.ForeignAuthorId).NotEmpty(); } private List<AlbumResource> GetAlbums() { - var artistIdQuery = Request.Query.ArtistId; - var albumIdsQuery = Request.Query.AlbumIds; - var foreignIdQuery = Request.Query.ForeignAlbumId; + var authorIdQuery = Request.Query.AuthorId; + var bookIdsQuery = Request.Query.BookIds; + var slugQuery = Request.Query.TitleSlug; var includeAllArtistAlbumsQuery = Request.Query.IncludeAllArtistAlbums; - if (!Request.Query.ArtistId.HasValue && !albumIdsQuery.HasValue && !foreignIdQuery.HasValue) + if (!Request.Query.AuthorId.HasValue && !bookIdsQuery.HasValue && !slugQuery.HasValue) { var albums = _albumService.GetAllAlbums(); - var artists = _artistService.GetAllArtists().ToDictionary(x => x.ArtistMetadataId); - var releases = _releaseService.GetAllReleases().GroupBy(x => x.AlbumId).ToDictionary(x => x.Key, y => y.ToList()); + var artists = _artistService.GetAllArtists().ToDictionary(x => x.AuthorMetadataId); foreach (var album in albums) { - album.Artist = artists[album.ArtistMetadataId]; - if (releases.TryGetValue(album.Id, out var albumReleases)) - { - album.AlbumReleases = albumReleases; - } - else - { - album.AlbumReleases = new List<AlbumRelease>(); - } + album.Author = artists[album.AuthorMetadataId]; } return MapToResource(albums, false); } - if (artistIdQuery.HasValue) + if (authorIdQuery.HasValue) { - int artistId = Convert.ToInt32(artistIdQuery.Value); + int authorId = Convert.ToInt32(authorIdQuery.Value); - return MapToResource(_albumService.GetAlbumsByArtist(artistId), false); + return MapToResource(_albumService.GetAlbumsByArtist(authorId), false); } - if (foreignIdQuery.HasValue) + if (slugQuery.HasValue) { - string foreignAlbumId = foreignIdQuery.Value.ToString(); + string titleSlug = slugQuery.Value.ToString(); - var album = _albumService.FindById(foreignAlbumId); + var album = _albumService.FindBySlug(titleSlug); if (album == null) { - return MapToResource(new List<Album>(), false); + return MapToResource(new List<Book>(), false); } if (includeAllArtistAlbumsQuery.HasValue && Convert.ToBoolean(includeAllArtistAlbumsQuery.Value)) { - return MapToResource(_albumService.GetAlbumsByArtist(album.ArtistId), false); + return MapToResource(_albumService.GetAlbumsByArtist(album.AuthorId), false); } else { - return MapToResource(new List<Album> { album }, false); + return MapToResource(new List<Book> { album }, false); } } - string albumIdsValue = albumIdsQuery.Value.ToString(); + string bookIdsValue = bookIdsQuery.Value.ToString(); - var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + var bookIds = bookIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(e => Convert.ToInt32(e)) .ToList(); - return MapToResource(_albumService.GetAlbums(albumIds), false); + return MapToResource(_albumService.GetAlbums(bookIds), false); } private int AddAlbum(AlbumResource albumResource) @@ -144,7 +132,6 @@ namespace Readarr.Api.V1.Albums var model = albumResource.ToModel(album); _albumService.UpdateAlbum(model); - _releaseService.UpdateMany(model.AlbumReleases.Value); BroadcastResourceChange(ModelAction.Updated, model.Id); } @@ -161,9 +148,9 @@ namespace Readarr.Api.V1.Albums { var resource = Request.Body.FromJson<AlbumsMonitoredResource>(); - _albumService.SetMonitored(resource.AlbumIds, resource.Monitored); + _albumService.SetMonitored(resource.BookIds, resource.Monitored); - return ResponseWithCode(MapToResource(_albumService.GetAlbums(resource.AlbumIds), false), HttpStatusCode.Accepted); + return ResponseWithCode(MapToResource(_albumService.GetAlbums(resource.BookIds), false), HttpStatusCode.Accepted); } public void Handle(AlbumGrabbedEvent message) diff --git a/src/Readarr.Api.V1/Albums/AlbumModuleWithSignalR.cs b/src/Readarr.Api.V1/Albums/AlbumModuleWithSignalR.cs index 7d43a9757..21331cee3 100644 --- a/src/Readarr.Api.V1/Albums/AlbumModuleWithSignalR.cs +++ b/src/Readarr.Api.V1/Albums/AlbumModuleWithSignalR.cs @@ -11,7 +11,7 @@ using Readarr.Http; namespace Readarr.Api.V1.Albums { - public abstract class AlbumModuleWithSignalR : ReadarrRestModuleWithSignalR<AlbumResource, Album> + public abstract class AlbumModuleWithSignalR : ReadarrRestModuleWithSignalR<AlbumResource, Book> { protected readonly IAlbumService _albumService; protected readonly IArtistStatisticsService _artistStatisticsService; @@ -56,13 +56,13 @@ namespace Readarr.Api.V1.Albums return resource; } - protected AlbumResource MapToResource(Album album, bool includeArtist) + protected AlbumResource MapToResource(Book album, bool includeArtist) { var resource = album.ToResource(); if (includeArtist) { - var artist = album.Artist.Value; + var artist = album.Author.Value; resource.Artist = artist.ToResource(); } @@ -73,19 +73,19 @@ namespace Readarr.Api.V1.Albums return resource; } - protected List<AlbumResource> MapToResource(List<Album> albums, bool includeArtist) + protected List<AlbumResource> MapToResource(List<Book> albums, bool includeArtist) { var result = albums.ToResource(); if (includeArtist) { - var artistDict = new Dictionary<int, NzbDrone.Core.Music.Artist>(); + var artistDict = new Dictionary<int, NzbDrone.Core.Music.Author>(); for (var i = 0; i < albums.Count; i++) { var album = albums[i]; var resource = result[i]; - var artist = artistDict.GetValueOrDefault(albums[i].ArtistMetadataId) ?? album.Artist?.Value; - artistDict[artist.ArtistMetadataId] = artist; + var artist = artistDict.GetValueOrDefault(albums[i].AuthorMetadataId) ?? album.Author?.Value; + artistDict[artist.AuthorMetadataId] = artist; resource.Artist = artist.ToResource(); } @@ -100,14 +100,14 @@ namespace Readarr.Api.V1.Albums private void FetchAndLinkAlbumStatistics(AlbumResource resource) { - LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.ArtistId)); + LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.AuthorId)); } private void LinkArtistStatistics(List<AlbumResource> resources, List<ArtistStatistics> artistStatistics) { foreach (var album in resources) { - var stats = artistStatistics.SingleOrDefault(ss => ss.ArtistId == album.ArtistId); + var stats = artistStatistics.SingleOrDefault(ss => ss.AuthorId == album.AuthorId); LinkArtistStatistics(album, stats); } } @@ -116,7 +116,7 @@ namespace Readarr.Api.V1.Albums { if (artistStatistics?.AlbumStatistics != null) { - var dictAlbumStats = artistStatistics.AlbumStatistics.ToDictionary(v => v.AlbumId); + var dictAlbumStats = artistStatistics.AlbumStatistics.ToDictionary(v => v.BookId); resource.Statistics = dictAlbumStats.GetValueOrDefault(resource.Id).ToResource(); } diff --git a/src/Readarr.Api.V1/Albums/AlbumReleaseResource.cs b/src/Readarr.Api.V1/Albums/AlbumReleaseResource.cs deleted file mode 100644 index 843e934ce..000000000 --- a/src/Readarr.Api.V1/Albums/AlbumReleaseResource.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Music; - -namespace Readarr.Api.V1.Albums -{ - public class AlbumReleaseResource - { - public int Id { get; set; } - public int AlbumId { get; set; } - public string ForeignReleaseId { get; set; } - public string Title { get; set; } - public string Status { get; set; } - public int Duration { get; set; } - public int TrackCount { get; set; } - public List<MediumResource> Media { get; set; } - public int MediumCount - { - get - { - if (Media == null) - { - return 0; - } - - return Media.Where(s => s.MediumNumber > 0).Count(); - } - } - - public string Disambiguation { get; set; } - public List<string> Country { get; set; } - public List<string> Label { get; set; } - public string Format { get; set; } - public bool Monitored { get; set; } - } - - public static class AlbumReleaseResourceMapper - { - public static AlbumReleaseResource ToResource(this AlbumRelease model) - { - if (model == null) - { - return null; - } - - return new AlbumReleaseResource - { - Id = model.Id, - AlbumId = model.AlbumId, - ForeignReleaseId = model.ForeignReleaseId, - Title = model.Title, - Status = model.Status, - Duration = model.Duration, - TrackCount = model.TrackCount, - Media = model.Media.ToResource(), - Disambiguation = model.Disambiguation, - Country = model.Country, - Label = model.Label, - Monitored = model.Monitored, - Format = string.Join(", ", - model.Media.OrderBy(x => x.Number) - .GroupBy(x => x.Format) - .Select(g => MediaFormatHelper(g.Key, g.Count())) - .ToList()) - }; - } - - public static AlbumRelease ToModel(this AlbumReleaseResource resource) - { - if (resource == null) - { - return null; - } - - return new AlbumRelease - { - Id = resource.Id, - AlbumId = resource.AlbumId, - ForeignReleaseId = resource.ForeignReleaseId, - Title = resource.Title, - Status = resource.Status, - Duration = resource.Duration, - Label = resource.Label, - Disambiguation = resource.Disambiguation, - Country = resource.Country, - Media = resource.Media.ToModel(), - TrackCount = resource.TrackCount, - Monitored = resource.Monitored - }; - } - - private static string MediaFormatHelper(string name, int count) - { - return count == 1 ? name : string.Join("x", new List<string> { count.ToString(), name }); - } - - public static List<AlbumReleaseResource> ToResource(this IEnumerable<AlbumRelease> models) - { - return models.Select(ToResource).ToList(); - } - - public static List<AlbumRelease> ToModel(this IEnumerable<AlbumReleaseResource> resources) - { - return resources.Select(ToModel).ToList(); - } - } -} diff --git a/src/Readarr.Api.V1/Albums/AlbumResource.cs b/src/Readarr.Api.V1/Albums/AlbumResource.cs index 9b1bcfbfb..6f9d2836c 100644 --- a/src/Readarr.Api.V1/Albums/AlbumResource.cs +++ b/src/Readarr.Api.V1/Albums/AlbumResource.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Music; using Readarr.Api.V1.Artist; +using Readarr.Api.V1.TrackFiles; using Readarr.Http.REST; namespace Readarr.Api.V1.Albums @@ -14,32 +15,18 @@ namespace Readarr.Api.V1.Albums public string Title { get; set; } public string Disambiguation { get; set; } public string Overview { get; set; } - public int ArtistId { get; set; } - public string ForeignAlbumId { get; set; } + public string Publisher { get; set; } + public string Language { get; set; } + public int AuthorId { get; set; } + public string ForeignBookId { get; set; } + public int GoodreadsId { get; set; } + public string TitleSlug { get; set; } + public string Isbn { get; set; } + public string Asin { get; set; } public bool Monitored { get; set; } - public bool AnyReleaseOk { get; set; } - public int ProfileId { get; set; } - public int Duration { get; set; } - public string AlbumType { get; set; } - public List<string> SecondaryTypes { get; set; } - public int MediumCount - { - get - { - if (Media == null) - { - return 0; - } - - return Media.Where(s => s.MediumNumber > 0).Count(); - } - } - public Ratings Ratings { get; set; } public DateTime? ReleaseDate { get; set; } - public List<AlbumReleaseResource> Releases { get; set; } public List<string> Genres { get; set; } - public List<MediumResource> Media { get; set; } public ArtistResource Artist { get; set; } public List<MediaCover> Images { get; set; } public List<Links> Links { get; set; } @@ -54,83 +41,82 @@ namespace Readarr.Api.V1.Albums public static class AlbumResourceMapper { - public static AlbumResource ToResource(this Album model) + public static AlbumResource ToResource(this Book model) { if (model == null) { return null; } - var selectedRelease = model.AlbumReleases?.Value.Where(x => x.Monitored).SingleOrDefault(); - return new AlbumResource { Id = model.Id, - ArtistId = model.ArtistId, - ForeignAlbumId = model.ForeignAlbumId, - ProfileId = model.ProfileId, + AuthorId = model.AuthorId, + ForeignBookId = model.ForeignBookId, + GoodreadsId = model.GoodreadsId, + TitleSlug = model.TitleSlug, + Asin = model.Asin, + Isbn = model.Isbn13, Monitored = model.Monitored, - AnyReleaseOk = model.AnyReleaseOk, ReleaseDate = model.ReleaseDate, Genres = model.Genres, Title = model.Title, Disambiguation = model.Disambiguation, Overview = model.Overview, + Publisher = model.Publisher, + Language = model.Language, Images = model.Images, Links = model.Links, Ratings = model.Ratings, - Duration = selectedRelease?.Duration ?? 0, - AlbumType = model.AlbumType, - SecondaryTypes = model.SecondaryTypes.Select(s => s.Name).ToList(), - Releases = model.AlbumReleases?.Value.ToResource() ?? new List<AlbumReleaseResource>(), - Media = selectedRelease?.Media.ToResource() ?? new List<MediumResource>(), - Artist = model.Artist?.Value.ToResource() + Artist = model.Author?.Value.ToResource() }; } - public static Album ToModel(this AlbumResource resource) + public static Book ToModel(this AlbumResource resource) { if (resource == null) { return null; } - var artist = resource.Artist?.ToModel() ?? new NzbDrone.Core.Music.Artist(); + var artist = resource.Artist?.ToModel() ?? new NzbDrone.Core.Music.Author(); - return new Album + return new Book { Id = resource.Id, - ForeignAlbumId = resource.ForeignAlbumId, + ForeignBookId = resource.ForeignBookId, + GoodreadsId = resource.GoodreadsId, + TitleSlug = resource.TitleSlug, + Asin = resource.Asin, + Isbn13 = resource.Isbn, Title = resource.Title, Disambiguation = resource.Disambiguation, Overview = resource.Overview, + Publisher = resource.Publisher, + Language = resource.Language, Images = resource.Images, - AlbumType = resource.AlbumType, Monitored = resource.Monitored, - AnyReleaseOk = resource.AnyReleaseOk, - AlbumReleases = resource.Releases.ToModel(), AddOptions = resource.AddOptions, - Artist = artist, - ArtistMetadata = artist.Metadata.Value + Author = artist, + AuthorMetadata = artist.Metadata.Value }; } - public static Album ToModel(this AlbumResource resource, Album album) + public static Book ToModel(this AlbumResource resource, Book album) { var updatedAlbum = resource.ToModel(); album.ApplyChanges(updatedAlbum); - album.AlbumReleases = updatedAlbum.AlbumReleases; return album; } - public static List<AlbumResource> ToResource(this IEnumerable<Album> models) + public static List<AlbumResource> ToResource(this IEnumerable<Book> models) { return models?.Select(ToResource).ToList(); } - public static List<Album> ToModel(this IEnumerable<AlbumResource> resources) + public static List<Book> ToModel(this IEnumerable<AlbumResource> resources) { return resources.Select(ToModel).ToList(); } diff --git a/src/Readarr.Api.V1/Albums/AlbumsMonitoredResource.cs b/src/Readarr.Api.V1/Albums/AlbumsMonitoredResource.cs index facef5ac0..e789f33da 100644 --- a/src/Readarr.Api.V1/Albums/AlbumsMonitoredResource.cs +++ b/src/Readarr.Api.V1/Albums/AlbumsMonitoredResource.cs @@ -4,7 +4,7 @@ namespace Readarr.Api.V1.Albums { public class AlbumsMonitoredResource { - public List<int> AlbumIds { get; set; } + public List<int> BookIds { get; set; } public bool Monitored { get; set; } } } diff --git a/src/Readarr.Api.V1/Albums/MediumResource.cs b/src/Readarr.Api.V1/Albums/MediumResource.cs deleted file mode 100644 index ff8f69d09..000000000 --- a/src/Readarr.Api.V1/Albums/MediumResource.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Music; - -namespace Readarr.Api.V1.Albums -{ - public class MediumResource - { - public int MediumNumber { get; set; } - public string MediumName { get; set; } - public string MediumFormat { get; set; } - } - - public static class MediumResourceMapper - { - public static MediumResource ToResource(this Medium model) - { - if (model == null) - { - return null; - } - - return new MediumResource - { - MediumNumber = model.Number, - MediumName = model.Name, - MediumFormat = model.Format - }; - } - - public static Medium ToModel(this MediumResource resource) - { - if (resource == null) - { - return null; - } - - return new Medium - { - Number = resource.MediumNumber, - Name = resource.MediumName, - Format = resource.MediumFormat - }; - } - - public static List<MediumResource> ToResource(this IEnumerable<Medium> models) - { - return models.Select(ToResource).ToList(); - } - - public static List<Medium> ToModel(this IEnumerable<MediumResource> resources) - { - return resources.Select(ToModel).ToList(); - } - } -} diff --git a/src/Readarr.Api.V1/Tracks/RenameTrackModule.cs b/src/Readarr.Api.V1/Albums/RenameBookModule.cs similarity index 62% rename from src/Readarr.Api.V1/Tracks/RenameTrackModule.cs rename to src/Readarr.Api.V1/Albums/RenameBookModule.cs index 42ab0cc93..9a4d10d69 100644 --- a/src/Readarr.Api.V1/Tracks/RenameTrackModule.cs +++ b/src/Readarr.Api.V1/Albums/RenameBookModule.cs @@ -3,7 +3,7 @@ using NzbDrone.Core.MediaFiles; using Readarr.Http; using Readarr.Http.REST; -namespace Readarr.Api.V1.Tracks +namespace Readarr.Api.V1.Albums { public class RenameTrackModule : ReadarrRestModule<RenameTrackResource> { @@ -19,24 +19,24 @@ namespace Readarr.Api.V1.Tracks private List<RenameTrackResource> GetTracks() { - int artistId; + int authorId; - if (Request.Query.ArtistId.HasValue) + if (Request.Query.AuthorId.HasValue) { - artistId = (int)Request.Query.ArtistId; + authorId = (int)Request.Query.AuthorId; } else { - throw new BadRequestException("artistId is missing"); + throw new BadRequestException("authorId is missing"); } - if (Request.Query.albumId.HasValue) + if (Request.Query.bookId.HasValue) { - var albumId = (int)Request.Query.albumId; - return _renameTrackFileService.GetRenamePreviews(artistId, albumId).ToResource(); + var bookId = (int)Request.Query.bookId; + return _renameTrackFileService.GetRenamePreviews(authorId, bookId).ToResource(); } - return _renameTrackFileService.GetRenamePreviews(artistId).ToResource(); + return _renameTrackFileService.GetRenamePreviews(authorId).ToResource(); } } } diff --git a/src/Readarr.Api.V1/Tracks/RenameTrackResource.cs b/src/Readarr.Api.V1/Albums/RenameBookResource.cs similarity index 76% rename from src/Readarr.Api.V1/Tracks/RenameTrackResource.cs rename to src/Readarr.Api.V1/Albums/RenameBookResource.cs index aeb87423c..3d6b67a1c 100644 --- a/src/Readarr.Api.V1/Tracks/RenameTrackResource.cs +++ b/src/Readarr.Api.V1/Albums/RenameBookResource.cs @@ -2,13 +2,12 @@ using System.Collections.Generic; using System.Linq; using Readarr.Http.REST; -namespace Readarr.Api.V1.Tracks +namespace Readarr.Api.V1.Albums { public class RenameTrackResource : RestResource { - public int ArtistId { get; set; } - public int AlbumId { get; set; } - public List<int> TrackNumbers { get; set; } + public int AuthorId { get; set; } + public int BookId { get; set; } public int TrackFileId { get; set; } public string ExistingPath { get; set; } public string NewPath { get; set; } @@ -25,9 +24,8 @@ namespace Readarr.Api.V1.Tracks return new RenameTrackResource { - ArtistId = model.ArtistId, - AlbumId = model.AlbumId, - TrackNumbers = model.TrackNumbers.ToList(), + AuthorId = model.AuthorId, + BookId = model.BookId, TrackFileId = model.TrackFileId, ExistingPath = model.ExistingPath, NewPath = model.NewPath diff --git a/src/Readarr.Api.V1/Tracks/RetagTrackModule.cs b/src/Readarr.Api.V1/Albums/RetagBookModule.cs similarity index 63% rename from src/Readarr.Api.V1/Tracks/RetagTrackModule.cs rename to src/Readarr.Api.V1/Albums/RetagBookModule.cs index 12fe51422..ecef7aed9 100644 --- a/src/Readarr.Api.V1/Tracks/RetagTrackModule.cs +++ b/src/Readarr.Api.V1/Albums/RetagBookModule.cs @@ -4,7 +4,7 @@ using NzbDrone.Core.MediaFiles; using Readarr.Http; using Readarr.Http.REST; -namespace Readarr.Api.V1.Tracks +namespace Readarr.Api.V1.Albums { public class RetagTrackModule : ReadarrRestModule<RetagTrackResource> { @@ -20,19 +20,19 @@ namespace Readarr.Api.V1.Tracks private List<RetagTrackResource> GetTracks() { - if (Request.Query.albumId.HasValue) + if (Request.Query.bookId.HasValue) { - var albumId = (int)Request.Query.albumId; - return _audioTagService.GetRetagPreviewsByAlbum(albumId).Where(x => x.Changes.Any()).ToResource(); + var bookId = (int)Request.Query.bookId; + return _audioTagService.GetRetagPreviewsByAlbum(bookId).Where(x => x.Changes.Any()).ToResource(); } - else if (Request.Query.ArtistId.HasValue) + else if (Request.Query.AuthorId.HasValue) { - var artistId = (int)Request.Query.ArtistId; - return _audioTagService.GetRetagPreviewsByArtist(artistId).Where(x => x.Changes.Any()).ToResource(); + var authorId = (int)Request.Query.AuthorId; + return _audioTagService.GetRetagPreviewsByArtist(authorId).Where(x => x.Changes.Any()).ToResource(); } else { - throw new BadRequestException("One of artistId or albumId must be specified"); + throw new BadRequestException("One of authorId or bookId must be specified"); } } } diff --git a/src/Readarr.Api.V1/Tracks/RetagTrackResource.cs b/src/Readarr.Api.V1/Albums/RetagBookResource.cs similarity index 88% rename from src/Readarr.Api.V1/Tracks/RetagTrackResource.cs rename to src/Readarr.Api.V1/Albums/RetagBookResource.cs index aa75cca2b..52cef0906 100644 --- a/src/Readarr.Api.V1/Tracks/RetagTrackResource.cs +++ b/src/Readarr.Api.V1/Albums/RetagBookResource.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using Readarr.Http.REST; -namespace Readarr.Api.V1.Tracks +namespace Readarr.Api.V1.Albums { public class TagDifference { @@ -13,8 +13,8 @@ namespace Readarr.Api.V1.Tracks public class RetagTrackResource : RestResource { - public int ArtistId { get; set; } - public int AlbumId { get; set; } + public int AuthorId { get; set; } + public int BookId { get; set; } public List<int> TrackNumbers { get; set; } public int TrackFileId { get; set; } public string Path { get; set; } @@ -32,8 +32,8 @@ namespace Readarr.Api.V1.Tracks return new RetagTrackResource { - ArtistId = model.ArtistId, - AlbumId = model.AlbumId, + AuthorId = model.AuthorId, + BookId = model.BookId, TrackNumbers = model.TrackNumbers.ToList(), TrackFileId = model.TrackFileId, Path = model.Path, diff --git a/src/Readarr.Api.V1/Artist/ArtistEditorDeleteResource.cs b/src/Readarr.Api.V1/Artist/ArtistEditorDeleteResource.cs index 1c63fa6f8..2465a91a5 100644 --- a/src/Readarr.Api.V1/Artist/ArtistEditorDeleteResource.cs +++ b/src/Readarr.Api.V1/Artist/ArtistEditorDeleteResource.cs @@ -4,7 +4,7 @@ namespace Readarr.Api.V1.Artist { public class ArtistEditorDeleteResource { - public List<int> ArtistIds { get; set; } + public List<int> AuthorIds { get; set; } public bool DeleteFiles { get; set; } } } diff --git a/src/Readarr.Api.V1/Artist/ArtistEditorModule.cs b/src/Readarr.Api.V1/Artist/ArtistEditorModule.cs index 6b14edced..6463d5ce5 100644 --- a/src/Readarr.Api.V1/Artist/ArtistEditorModule.cs +++ b/src/Readarr.Api.V1/Artist/ArtistEditorModule.cs @@ -26,7 +26,7 @@ namespace Readarr.Api.V1.Artist private object SaveAll() { var resource = Request.Body.FromJson<ArtistEditorResource>(); - var artistToUpdate = _artistService.GetArtists(resource.ArtistIds); + var artistToUpdate = _artistService.GetArtists(resource.AuthorIds); var artistToMove = new List<BulkMoveArtist>(); foreach (var artist in artistToUpdate) @@ -46,17 +46,12 @@ namespace Readarr.Api.V1.Artist artist.MetadataProfileId = resource.MetadataProfileId.Value; } - if (resource.AlbumFolder.HasValue) - { - artist.AlbumFolder = resource.AlbumFolder.Value; - } - if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) { artist.RootFolderPath = resource.RootFolderPath; artistToMove.Add(new BulkMoveArtist { - ArtistId = artist.Id, + AuthorId = artist.Id, SourcePath = artist.Path }); } @@ -99,9 +94,9 @@ namespace Readarr.Api.V1.Artist { var resource = Request.Body.FromJson<ArtistEditorResource>(); - foreach (var artistId in resource.ArtistIds) + foreach (var authorId in resource.AuthorIds) { - _artistService.DeleteArtist(artistId, false); + _artistService.DeleteArtist(authorId, false); } return new object(); diff --git a/src/Readarr.Api.V1/Artist/ArtistEditorResource.cs b/src/Readarr.Api.V1/Artist/ArtistEditorResource.cs index c2bc7ccd6..b558ed1ef 100644 --- a/src/Readarr.Api.V1/Artist/ArtistEditorResource.cs +++ b/src/Readarr.Api.V1/Artist/ArtistEditorResource.cs @@ -4,7 +4,7 @@ namespace Readarr.Api.V1.Artist { public class ArtistEditorResource { - public List<int> ArtistIds { get; set; } + public List<int> AuthorIds { get; set; } public bool? Monitored { get; set; } public int? QualityProfileId { get; set; } public int? MetadataProfileId { get; set; } diff --git a/src/Readarr.Api.V1/Artist/ArtistLookupModule.cs b/src/Readarr.Api.V1/Artist/ArtistLookupModule.cs index e09420c28..a606f2925 100644 --- a/src/Readarr.Api.V1/Artist/ArtistLookupModule.cs +++ b/src/Readarr.Api.V1/Artist/ArtistLookupModule.cs @@ -9,9 +9,9 @@ namespace Readarr.Api.V1.Artist { public class ArtistLookupModule : ReadarrRestModule<ArtistResource> { - private readonly ISearchForNewArtist _searchProxy; + private readonly ISearchForNewAuthor _searchProxy; - public ArtistLookupModule(ISearchForNewArtist searchProxy) + public ArtistLookupModule(ISearchForNewAuthor searchProxy) : base("/artist/lookup") { _searchProxy = searchProxy; @@ -20,11 +20,11 @@ namespace Readarr.Api.V1.Artist private object Search() { - var searchResults = _searchProxy.SearchForNewArtist((string)Request.Query.term); + var searchResults = _searchProxy.SearchForNewAuthor((string)Request.Query.term); return MapToResource(searchResults).ToList(); } - private static IEnumerable<ArtistResource> MapToResource(IEnumerable<NzbDrone.Core.Music.Artist> artist) + private static IEnumerable<ArtistResource> MapToResource(IEnumerable<NzbDrone.Core.Music.Author> artist) { foreach (var currentArtist in artist) { diff --git a/src/Readarr.Api.V1/Artist/ArtistModule.cs b/src/Readarr.Api.V1/Artist/ArtistModule.cs index 227b0fcaa..f84cbdbd2 100644 --- a/src/Readarr.Api.V1/Artist/ArtistModule.cs +++ b/src/Readarr.Api.V1/Artist/ArtistModule.cs @@ -21,7 +21,7 @@ using Readarr.Http.Extensions; namespace Readarr.Api.V1.Artist { - public class ArtistModule : ReadarrRestModuleWithSignalR<ArtistResource, NzbDrone.Core.Music.Artist>, + public class ArtistModule : ReadarrRestModuleWithSignalR<ArtistResource, NzbDrone.Core.Music.Author>, IHandle<AlbumImportedEvent>, IHandle<AlbumEditedEvent>, IHandle<TrackFileDeletedEvent>, @@ -91,7 +91,7 @@ namespace Readarr.Api.V1.Artist PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.ArtistName).NotEmpty(); - PostValidator.RuleFor(s => s.ForeignArtistId).NotEmpty().SetValidator(artistExistsValidator); + PostValidator.RuleFor(s => s.ForeignAuthorId).NotEmpty().SetValidator(artistExistsValidator); PutValidator.RuleFor(s => s.Path).IsValidPath(); } @@ -102,7 +102,7 @@ namespace Readarr.Api.V1.Artist return GetArtistResource(artist); } - private ArtistResource GetArtistResource(NzbDrone.Core.Music.Artist artist) + private ArtistResource GetArtistResource(NzbDrone.Core.Music.Author artist) { if (artist == null) { @@ -152,7 +152,7 @@ namespace Readarr.Api.V1.Artist _commandQueueManager.Push(new MoveArtistCommand { - ArtistId = artist.Id, + AuthorId = artist.Id, SourcePath = sourcePath, DestinationPath = destinationPath, Trigger = CommandTrigger.Manual @@ -189,8 +189,8 @@ namespace Readarr.Api.V1.Artist foreach (var artistResource in artists) { - artistResource.NextAlbum = nextAlbums.FirstOrDefault(x => x.ArtistMetadataId == artistResource.ArtistMetadataId); - artistResource.LastAlbum = lastAlbums.FirstOrDefault(x => x.ArtistMetadataId == artistResource.ArtistMetadataId); + artistResource.NextAlbum = nextAlbums.FirstOrDefault(x => x.AuthorMetadataId == artistResource.ArtistMetadataId); + artistResource.LastAlbum = lastAlbums.FirstOrDefault(x => x.AuthorMetadataId == artistResource.ArtistMetadataId); } } @@ -203,7 +203,7 @@ namespace Readarr.Api.V1.Artist { foreach (var artist in resources) { - var stats = artistStatistics.SingleOrDefault(ss => ss.ArtistId == artist.Id); + var stats = artistStatistics.SingleOrDefault(ss => ss.AuthorId == artist.Id); if (stats == null) { continue; @@ -246,7 +246,7 @@ namespace Readarr.Api.V1.Artist public void Handle(AlbumEditedEvent message) { - BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Album.Artist.Value)); + BroadcastResourceChange(ModelAction.Updated, GetArtistResource(message.Album.Author.Value)); } public void Handle(TrackFileDeletedEvent message) diff --git a/src/Readarr.Api.V1/Artist/ArtistResource.cs b/src/Readarr.Api.V1/Artist/ArtistResource.cs index 1dda26bb7..f740bc8e4 100644 --- a/src/Readarr.Api.V1/Artist/ArtistResource.cs +++ b/src/Readarr.Api.V1/Artist/ArtistResource.cs @@ -21,21 +21,18 @@ namespace Readarr.Api.V1.Artist public bool Ended => Status == ArtistStatusType.Ended; public string ArtistName { get; set; } - public string ForeignArtistId { get; set; } - public string MBId { get; set; } - public int TADBId { get; set; } - public int DiscogsId { get; set; } - public string AllMusicId { get; set; } + public string ForeignAuthorId { get; set; } + public int GoodreadsId { get; set; } + public string TitleSlug { get; set; } public string Overview { get; set; } public string ArtistType { get; set; } public string Disambiguation { get; set; } public List<Links> Links { get; set; } - public Album NextAlbum { get; set; } - public Album LastAlbum { get; set; } + public Book NextAlbum { get; set; } + public Book LastAlbum { get; set; } public List<MediaCover> Images { get; set; } - public List<Member> Members { get; set; } public string RemotePoster { get; set; } @@ -62,7 +59,7 @@ namespace Readarr.Api.V1.Artist public static class ArtistResourceMapper { - public static ArtistResource ToResource(this NzbDrone.Core.Music.Artist model) + public static ArtistResource ToResource(this NzbDrone.Core.Music.Author model) { if (model == null) { @@ -72,7 +69,7 @@ namespace Readarr.Api.V1.Artist return new ArtistResource { Id = model.Id, - ArtistMetadataId = model.ArtistMetadataId, + ArtistMetadataId = model.AuthorMetadataId, ArtistName = model.Name, @@ -91,11 +88,12 @@ namespace Readarr.Api.V1.Artist MetadataProfileId = model.MetadataProfileId, Links = model.Metadata.Value.Links, - AlbumFolder = model.AlbumFolder, Monitored = model.Monitored, CleanName = model.CleanName, - ForeignArtistId = model.Metadata.Value.ForeignArtistId, + ForeignAuthorId = model.Metadata.Value.ForeignAuthorId, + GoodreadsId = model.Metadata.Value.GoodreadsId, + TitleSlug = model.Metadata.Value.TitleSlug, // Root folder path is now calculated from the artist path // RootFolderPath = model.RootFolderPath, @@ -109,20 +107,22 @@ namespace Readarr.Api.V1.Artist }; } - public static NzbDrone.Core.Music.Artist ToModel(this ArtistResource resource) + public static NzbDrone.Core.Music.Author ToModel(this ArtistResource resource) { if (resource == null) { return null; } - return new NzbDrone.Core.Music.Artist + return new NzbDrone.Core.Music.Author { Id = resource.Id, - Metadata = new NzbDrone.Core.Music.ArtistMetadata + Metadata = new NzbDrone.Core.Music.AuthorMetadata { - ForeignArtistId = resource.ForeignArtistId, + ForeignAuthorId = resource.ForeignAuthorId, + GoodreadsId = resource.GoodreadsId, + TitleSlug = resource.TitleSlug, Name = resource.ArtistName, Status = resource.Status, Overview = resource.Overview, @@ -139,7 +139,6 @@ namespace Readarr.Api.V1.Artist QualityProfileId = resource.QualityProfileId, MetadataProfileId = resource.MetadataProfileId, - AlbumFolder = resource.AlbumFolder, Monitored = resource.Monitored, CleanName = resource.CleanName, @@ -151,7 +150,7 @@ namespace Readarr.Api.V1.Artist }; } - public static NzbDrone.Core.Music.Artist ToModel(this ArtistResource resource, NzbDrone.Core.Music.Artist artist) + public static NzbDrone.Core.Music.Author ToModel(this ArtistResource resource, NzbDrone.Core.Music.Author artist) { var updatedArtist = resource.ToModel(); @@ -160,12 +159,12 @@ namespace Readarr.Api.V1.Artist return artist; } - public static List<ArtistResource> ToResource(this IEnumerable<NzbDrone.Core.Music.Artist> artist) + public static List<ArtistResource> ToResource(this IEnumerable<NzbDrone.Core.Music.Author> artist) { return artist.Select(ToResource).ToList(); } - public static List<NzbDrone.Core.Music.Artist> ToModel(this IEnumerable<ArtistResource> resources) + public static List<NzbDrone.Core.Music.Author> ToModel(this IEnumerable<ArtistResource> resources) { return resources.Select(ToModel).ToList(); } diff --git a/src/Readarr.Api.V1/Blacklist/BlacklistResource.cs b/src/Readarr.Api.V1/Blacklist/BlacklistResource.cs index 01d9d0214..9918a1cea 100644 --- a/src/Readarr.Api.V1/Blacklist/BlacklistResource.cs +++ b/src/Readarr.Api.V1/Blacklist/BlacklistResource.cs @@ -9,8 +9,8 @@ namespace Readarr.Api.V1.Blacklist { public class BlacklistResource : RestResource { - public int ArtistId { get; set; } - public List<int> AlbumIds { get; set; } + public int AuthorId { get; set; } + public List<int> BookIds { get; set; } public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } @@ -34,8 +34,8 @@ namespace Readarr.Api.V1.Blacklist { Id = model.Id, - ArtistId = model.ArtistId, - AlbumIds = model.AlbumIds, + AuthorId = model.AuthorId, + BookIds = model.BookIds, SourceTitle = model.SourceTitle, Quality = model.Quality, Date = model.Date, diff --git a/src/Readarr.Api.V1/Calendar/CalendarFeedModule.cs b/src/Readarr.Api.V1/Calendar/CalendarFeedModule.cs index 393fe1d9c..231417134 100644 --- a/src/Readarr.Api.V1/Calendar/CalendarFeedModule.cs +++ b/src/Readarr.Api.V1/Calendar/CalendarFeedModule.cs @@ -64,7 +64,7 @@ namespace Readarr.Api.V1.Calendar var albums = _albumService.AlbumsBetweenDates(start, end, unmonitored); var calendar = new Ical.Net.Calendar { - ProductId = "-//readarr.audio//Readarr//EN" + ProductId = "-//readarr.com//Readarr//EN" }; var calendarName = "Readarr Music Schedule"; @@ -73,7 +73,7 @@ namespace Readarr.Api.V1.Calendar foreach (var album in albums.OrderBy(v => v.ReleaseDate.Value)) { - var artist = _artistService.GetArtist(album.ArtistId); // Temp fix TODO: Figure out why Album.Artist is not populated during AlbumsBetweenDates Query + var artist = _artistService.GetArtist(album.AuthorId); // Temp fix TODO: Figure out why Album.Artist is not populated during AlbumsBetweenDates Query if (tags.Any() && tags.None(artist.Tags.Contains)) { diff --git a/src/Readarr.Api.V1/Config/NamingConfigModule.cs b/src/Readarr.Api.V1/Config/NamingConfigModule.cs index abdfd8326..82e4318fc 100644 --- a/src/Readarr.Api.V1/Config/NamingConfigModule.cs +++ b/src/Readarr.Api.V1/Config/NamingConfigModule.cs @@ -17,9 +17,9 @@ namespace Readarr.Api.V1.Config private readonly IBuildFileNames _filenameBuilder; public NamingConfigModule(INamingConfigService namingConfigService, - IFilenameSampleService filenameSampleService, - IFilenameValidationService filenameValidationService, - IBuildFileNames filenameBuilder) + IFilenameSampleService filenameSampleService, + IFilenameValidationService filenameValidationService, + IBuildFileNames filenameBuilder) : base("config/naming") { _namingConfigService = namingConfigService; @@ -33,9 +33,7 @@ namespace Readarr.Api.V1.Config Get("/examples", x => GetExamples(this.Bind<NamingConfigResource>())); SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat(); - SharedValidator.RuleFor(c => c.MultiDiscTrackFormat).ValidTrackFormat(); SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat(); - SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidAlbumFolderFormat(); } private void UpdateNamingConfig(NamingConfigResource resource) @@ -57,12 +55,6 @@ namespace Readarr.Api.V1.Config basicConfig.AddToResource(resource); } - if (resource.MultiDiscTrackFormat.IsNotNullOrWhiteSpace()) - { - var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec); - basicConfig.AddToResource(resource); - } - return resource; } @@ -82,24 +74,15 @@ namespace Readarr.Api.V1.Config var sampleResource = new NamingExampleResource(); var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); - var multiDiscTrackSampleResult = _filenameSampleService.GetMultiDiscTrackSample(nameSpec); sampleResource.SingleTrackExample = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult) != null ? null : singleTrackSampleResult.FileName; - sampleResource.MultiDiscTrackExample = _filenameValidationService.ValidateTrackFilename(multiDiscTrackSampleResult) != null - ? null - : multiDiscTrackSampleResult.FileName; - sampleResource.ArtistFolderExample = nameSpec.ArtistFolderFormat.IsNullOrWhiteSpace() ? null : _filenameSampleService.GetArtistFolderSample(nameSpec); - sampleResource.AlbumFolderExample = nameSpec.AlbumFolderFormat.IsNullOrWhiteSpace() - ? null - : _filenameSampleService.GetAlbumFolderSample(nameSpec); - return sampleResource; } diff --git a/src/Readarr.Api.V1/Config/NamingConfigResource.cs b/src/Readarr.Api.V1/Config/NamingConfigResource.cs index 547bee15d..1b32e6cbb 100644 --- a/src/Readarr.Api.V1/Config/NamingConfigResource.cs +++ b/src/Readarr.Api.V1/Config/NamingConfigResource.cs @@ -7,9 +7,7 @@ namespace Readarr.Api.V1.Config public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } public string StandardTrackFormat { get; set; } - public string MultiDiscTrackFormat { get; set; } public string ArtistFolderFormat { get; set; } - public string AlbumFolderFormat { get; set; } public bool IncludeArtistName { get; set; } public bool IncludeAlbumTitle { get; set; } public bool IncludeQuality { get; set; } diff --git a/src/Readarr.Api.V1/Config/NamingExampleResource.cs b/src/Readarr.Api.V1/Config/NamingExampleResource.cs index 1fa7af799..615a8bf10 100644 --- a/src/Readarr.Api.V1/Config/NamingExampleResource.cs +++ b/src/Readarr.Api.V1/Config/NamingExampleResource.cs @@ -5,9 +5,7 @@ namespace Readarr.Api.V1.Config public class NamingExampleResource { public string SingleTrackExample { get; set; } - public string MultiDiscTrackExample { get; set; } public string ArtistFolderExample { get; set; } - public string AlbumFolderExample { get; set; } } public static class NamingConfigResourceMapper @@ -21,9 +19,7 @@ namespace Readarr.Api.V1.Config RenameTracks = model.RenameTracks, ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, StandardTrackFormat = model.StandardTrackFormat, - MultiDiscTrackFormat = model.MultiDiscTrackFormat, - ArtistFolderFormat = model.ArtistFolderFormat, - AlbumFolderFormat = model.AlbumFolderFormat + ArtistFolderFormat = model.ArtistFolderFormat }; } @@ -46,10 +42,7 @@ namespace Readarr.Api.V1.Config RenameTracks = resource.RenameTracks, ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, StandardTrackFormat = resource.StandardTrackFormat, - MultiDiscTrackFormat = resource.MultiDiscTrackFormat, - ArtistFolderFormat = resource.ArtistFolderFormat, - AlbumFolderFormat = resource.AlbumFolderFormat }; } } diff --git a/src/Readarr.Api.V1/History/HistoryModule.cs b/src/Readarr.Api.V1/History/HistoryModule.cs index cb63614b0..78949625f 100644 --- a/src/Readarr.Api.V1/History/HistoryModule.cs +++ b/src/Readarr.Api.V1/History/HistoryModule.cs @@ -8,7 +8,6 @@ using NzbDrone.Core.Download; using NzbDrone.Core.History; using Readarr.Api.V1.Albums; using Readarr.Api.V1.Artist; -using Readarr.Api.V1.Tracks; using Readarr.Http; using Readarr.Http.Extensions; using Readarr.Http.REST; @@ -35,7 +34,7 @@ namespace Readarr.Api.V1.History Post("/failed", x => MarkAsFailed()); } - protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeArtist, bool includeAlbum, bool includeTrack) + protected HistoryResource MapToResource(NzbDrone.Core.History.History model, bool includeArtist, bool includeAlbum) { var resource = model.ToResource(); @@ -49,11 +48,6 @@ namespace Readarr.Api.V1.History resource.Album = model.Album.ToResource(); } - if (includeTrack) - { - resource.Track = model.Track.ToResource(); - } - if (model.Artist != null) { resource.QualityCutoffNotMet = _upgradableSpecification.QualityCutoffNotMet(model.Artist.QualityProfile.Value, model.Quality); @@ -67,10 +61,9 @@ namespace Readarr.Api.V1.History var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending); var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum"); - var includeTrack = Request.GetBooleanQueryParameter("includeTrack"); var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType"); - var albumIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "albumId"); + var bookIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "bookId"); var downloadIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "downloadId"); if (eventTypeFilter != null) @@ -79,10 +72,10 @@ namespace Readarr.Api.V1.History pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue); } - if (albumIdFilter != null) + if (bookIdFilter != null) { - var albumId = Convert.ToInt32(albumIdFilter.Value); - pagingSpec.FilterExpressions.Add(h => h.AlbumId == albumId); + var bookId = Convert.ToInt32(bookIdFilter.Value); + pagingSpec.FilterExpressions.Add(h => h.BookId == bookId); } if (downloadIdFilter != null) @@ -91,7 +84,7 @@ namespace Readarr.Api.V1.History pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId); } - return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeArtist, includeAlbum, includeTrack)); + return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeArtist, includeAlbum)); } private List<HistoryResource> GetHistorySince() @@ -108,46 +101,44 @@ namespace Readarr.Api.V1.History HistoryEventType? eventType = null; var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum"); - var includeTrack = Request.GetBooleanQueryParameter("includeTrack"); if (queryEventType.HasValue) { eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value); } - return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList(); + return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum)).ToList(); } private List<HistoryResource> GetArtistHistory() { - var queryArtistId = Request.Query.ArtistId; - var queryAlbumId = Request.Query.AlbumId; + var queryAuthorId = Request.Query.AuthorId; + var queryBookId = Request.Query.BookId; var queryEventType = Request.Query.EventType; - if (!queryArtistId.HasValue) + if (!queryAuthorId.HasValue) { - throw new BadRequestException("artistId is missing"); + throw new BadRequestException("authorId is missing"); } - int artistId = Convert.ToInt32(queryArtistId.Value); + int authorId = Convert.ToInt32(queryAuthorId.Value); HistoryEventType? eventType = null; var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum"); - var includeTrack = Request.GetBooleanQueryParameter("includeTrack"); if (queryEventType.HasValue) { eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value); } - if (queryAlbumId.HasValue) + if (queryBookId.HasValue) { - int albumId = Convert.ToInt32(queryAlbumId.Value); + int bookId = Convert.ToInt32(queryBookId.Value); - return _historyService.GetByAlbum(albumId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList(); + return _historyService.GetByAlbum(bookId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum)).ToList(); } - return _historyService.GetByArtist(artistId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList(); + return _historyService.GetByArtist(authorId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum)).ToList(); } private object MarkAsFailed() diff --git a/src/Readarr.Api.V1/History/HistoryResource.cs b/src/Readarr.Api.V1/History/HistoryResource.cs index 8d2ed4c68..83fdd034a 100644 --- a/src/Readarr.Api.V1/History/HistoryResource.cs +++ b/src/Readarr.Api.V1/History/HistoryResource.cs @@ -4,16 +4,14 @@ using NzbDrone.Core.History; using NzbDrone.Core.Qualities; using Readarr.Api.V1.Albums; using Readarr.Api.V1.Artist; -using Readarr.Api.V1.Tracks; using Readarr.Http.REST; namespace Readarr.Api.V1.History { public class HistoryResource : RestResource { - public int AlbumId { get; set; } - public int ArtistId { get; set; } - public int TrackId { get; set; } + public int BookId { get; set; } + public int AuthorId { get; set; } public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public bool QualityCutoffNotMet { get; set; } @@ -26,7 +24,6 @@ namespace Readarr.Api.V1.History public AlbumResource Album { get; set; } public ArtistResource Artist { get; set; } - public TrackResource Track { get; set; } } public static class HistoryResourceMapper @@ -42,9 +39,8 @@ namespace Readarr.Api.V1.History { Id = model.Id, - AlbumId = model.AlbumId, - ArtistId = model.ArtistId, - TrackId = model.TrackId, + BookId = model.BookId, + AuthorId = model.AuthorId, SourceTitle = model.SourceTitle, Quality = model.Quality, diff --git a/src/Readarr.Api.V1/Indexers/ReleaseModule.cs b/src/Readarr.Api.V1/Indexers/ReleaseModule.cs index 5d3a27950..8494dd085 100644 --- a/src/Readarr.Api.V1/Indexers/ReleaseModule.cs +++ b/src/Readarr.Api.V1/Indexers/ReleaseModule.cs @@ -76,24 +76,24 @@ namespace Readarr.Api.V1.Indexers private List<ReleaseResource> GetReleases() { - if (Request.Query.albumId.HasValue) + if (Request.Query.bookId.HasValue) { - return GetAlbumReleases(Request.Query.albumId); + return GetAlbumReleases(Request.Query.bookId); } - if (Request.Query.artistId.HasValue) + if (Request.Query.authorId.HasValue) { - return GetArtistReleases(Request.Query.artistId); + return GetArtistReleases(Request.Query.authorId); } return GetRss(); } - private List<ReleaseResource> GetAlbumReleases(int albumId) + private List<ReleaseResource> GetAlbumReleases(int bookId) { try { - var decisions = _nzbSearchService.AlbumSearch(albumId, true, true, true); + var decisions = _nzbSearchService.AlbumSearch(bookId, true, true, true); var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions); return MapDecisions(prioritizedDecisions); @@ -106,11 +106,11 @@ namespace Readarr.Api.V1.Indexers return new List<ReleaseResource>(); } - private List<ReleaseResource> GetArtistReleases(int artistId) + private List<ReleaseResource> GetArtistReleases(int authorId) { try { - var decisions = _nzbSearchService.ArtistSearch(artistId, false, true, true); + var decisions = _nzbSearchService.ArtistSearch(authorId, false, true, true); var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions); return MapDecisions(prioritizedDecisions); diff --git a/src/Readarr.Api.V1/Indexers/ReleaseResource.cs b/src/Readarr.Api.V1/Indexers/ReleaseResource.cs index 6e3ed8e4d..5e7e12e3c 100644 --- a/src/Readarr.Api.V1/Indexers/ReleaseResource.cs +++ b/src/Readarr.Api.V1/Indexers/ReleaseResource.cs @@ -52,12 +52,12 @@ namespace Readarr.Api.V1.Indexers [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] // [JsonIgnore] - public int? ArtistId { get; set; } + public int? AuthorId { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] // [JsonIgnore] - public int? AlbumId { get; set; } + public int? BookId { get; set; } } public static class ReleaseResourceMapper diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportModule.cs b/src/Readarr.Api.V1/ManualImport/ManualImportModule.cs index e9efefc10..556abdd41 100644 --- a/src/Readarr.Api.V1/ManualImport/ManualImportModule.cs +++ b/src/Readarr.Api.V1/ManualImport/ManualImportModule.cs @@ -15,19 +15,16 @@ namespace Readarr.Api.V1.ManualImport { private readonly IArtistService _artistService; private readonly IAlbumService _albumService; - private readonly IReleaseService _releaseService; private readonly IManualImportService _manualImportService; private readonly Logger _logger; public ManualImportModule(IManualImportService manualImportService, IArtistService artistService, IAlbumService albumService, - IReleaseService releaseService, Logger logger) { _artistService = artistService; _albumService = albumService; - _releaseService = releaseService; _manualImportService = manualImportService; _logger = logger; @@ -75,12 +72,10 @@ namespace Readarr.Api.V1.ManualImport Size = resource.Size, Artist = resource.Artist == null ? null : _artistService.GetArtist(resource.Artist.Id), Album = resource.Album == null ? null : _albumService.GetAlbum(resource.Album.Id), - Release = resource.AlbumReleaseId == 0 ? null : _releaseService.GetRelease(resource.AlbumReleaseId), Quality = resource.Quality, DownloadId = resource.DownloadId, AdditionalFile = resource.AdditionalFile, - ReplaceExistingFiles = resource.ReplaceExistingFiles, - DisableReleaseSwitching = resource.DisableReleaseSwitching + ReplaceExistingFiles = resource.ReplaceExistingFiles }); } diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs index 64b72913d..0f13b5b69 100644 --- a/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs +++ b/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs @@ -6,7 +6,6 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using Readarr.Api.V1.Albums; using Readarr.Api.V1.Artist; -using Readarr.Api.V1.Tracks; using Readarr.Http.REST; namespace Readarr.Api.V1.ManualImport @@ -18,8 +17,6 @@ namespace Readarr.Api.V1.ManualImport public long Size { get; set; } public ArtistResource Artist { get; set; } public AlbumResource Album { get; set; } - public int AlbumReleaseId { get; set; } - public List<TrackResource> Tracks { get; set; } public QualityModel Quality { get; set; } public int QualityWeight { get; set; } public string DownloadId { get; set; } @@ -27,7 +24,6 @@ namespace Readarr.Api.V1.ManualImport public ParsedTrackInfo AudioTags { get; set; } public bool AdditionalFile { get; set; } public bool ReplaceExistingFiles { get; set; } - public bool DisableReleaseSwitching { get; set; } } public static class ManualImportResourceMapper @@ -47,8 +43,6 @@ namespace Readarr.Api.V1.ManualImport Size = model.Size, Artist = model.Artist.ToResource(), Album = model.Album.ToResource(), - AlbumReleaseId = model.Release?.Id ?? 0, - Tracks = model.Tracks.ToResource(), Quality = model.Quality, //QualityWeight @@ -56,8 +50,7 @@ namespace Readarr.Api.V1.ManualImport Rejections = model.Rejections, AudioTags = model.Tags, AdditionalFile = model.AdditionalFile, - ReplaceExistingFiles = model.ReplaceExistingFiles, - DisableReleaseSwitching = model.DisableReleaseSwitching + ReplaceExistingFiles = model.ReplaceExistingFiles }; } diff --git a/src/Readarr.Api.V1/MediaCovers/MediaCoverModule.cs b/src/Readarr.Api.V1/MediaCovers/MediaCoverModule.cs index 806842268..d90611ddc 100644 --- a/src/Readarr.Api.V1/MediaCovers/MediaCoverModule.cs +++ b/src/Readarr.Api.V1/MediaCovers/MediaCoverModule.cs @@ -10,8 +10,8 @@ namespace Readarr.Api.V1.MediaCovers { public class MediaCoverModule : ReadarrV1Module { - private const string MEDIA_COVER_ARTIST_ROUTE = @"/Artist/(?<artistId>\d+)/(?<filename>(.+)\.(jpg|png|gif))"; - private const string MEDIA_COVER_ALBUM_ROUTE = @"/Album/(?<artistId>\d+)/(?<filename>(.+)\.(jpg|png|gif))"; + private const string MEDIA_COVER_ARTIST_ROUTE = @"/Artist/(?<authorId>\d+)/(?<filename>(.+)\.(jpg|png|gif))"; + private const string MEDIA_COVER_ALBUM_ROUTE = @"/Album/(?<authorId>\d+)/(?<filename>(.+)\.(jpg|png|gif))"; private static readonly Regex RegexResizedImage = new Regex(@"-\d+(?=\.(jpg|png|gif)$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -24,13 +24,13 @@ namespace Readarr.Api.V1.MediaCovers _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; - Get(MEDIA_COVER_ARTIST_ROUTE, options => GetArtistMediaCover(options.artistId, options.filename)); - Get(MEDIA_COVER_ALBUM_ROUTE, options => GetAlbumMediaCover(options.artistId, options.filename)); + Get(MEDIA_COVER_ARTIST_ROUTE, options => GetArtistMediaCover(options.authorId, options.filename)); + Get(MEDIA_COVER_ALBUM_ROUTE, options => GetAlbumMediaCover(options.authorId, options.filename)); } - private object GetArtistMediaCover(int artistId, string filename) + private object GetArtistMediaCover(int authorId, string filename) { - var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", artistId.ToString(), filename); + var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", authorId.ToString(), filename); if (!_diskProvider.FileExists(filePath) || _diskProvider.GetFileSize(filePath) == 0) { @@ -48,9 +48,9 @@ namespace Readarr.Api.V1.MediaCovers return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath)); } - private object GetAlbumMediaCover(int albumId, string filename) + private object GetAlbumMediaCover(int bookId, string filename) { - var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", "Albums", albumId.ToString(), filename); + var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", "Albums", bookId.ToString(), filename); if (!_diskProvider.FileExists(filePath) || _diskProvider.GetFileSize(filePath) == 0) { diff --git a/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileModule.cs b/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileModule.cs index dbba38b2a..9b64a05f3 100644 --- a/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileModule.cs +++ b/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileModule.cs @@ -13,9 +13,6 @@ namespace Readarr.Api.V1.Profiles.Metadata { _profileService = profileService; SharedValidator.RuleFor(c => c.Name).NotEqual("None").WithMessage("'None' is a reserved profile name").NotEmpty(); - SharedValidator.RuleFor(c => c.PrimaryAlbumTypes).MustHaveAllowedPrimaryType(); - SharedValidator.RuleFor(c => c.SecondaryAlbumTypes).MustHaveAllowedSecondaryType(); - SharedValidator.RuleFor(c => c.ReleaseStatuses).MustHaveAllowedReleaseStatus(); GetResourceAll = GetAll; GetResourceById = GetById; diff --git a/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileResource.cs b/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileResource.cs index f9d50d174..f1c9d5b4e 100644 --- a/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileResource.cs +++ b/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileResource.cs @@ -8,27 +8,13 @@ namespace Readarr.Api.V1.Profiles.Metadata public class MetadataProfileResource : RestResource { public string Name { get; set; } - public List<ProfilePrimaryAlbumTypeItemResource> PrimaryAlbumTypes { get; set; } - public List<ProfileSecondaryAlbumTypeItemResource> SecondaryAlbumTypes { get; set; } - public List<ProfileReleaseStatusItemResource> ReleaseStatuses { get; set; } - } - - public class ProfilePrimaryAlbumTypeItemResource : RestResource - { - public NzbDrone.Core.Music.PrimaryAlbumType AlbumType { get; set; } - public bool Allowed { get; set; } - } - - public class ProfileSecondaryAlbumTypeItemResource : RestResource - { - public NzbDrone.Core.Music.SecondaryAlbumType AlbumType { get; set; } - public bool Allowed { get; set; } - } - - public class ProfileReleaseStatusItemResource : RestResource - { - public NzbDrone.Core.Music.ReleaseStatus ReleaseStatus { get; set; } - public bool Allowed { get; set; } + public double MinRating { get; set; } + public int MinRatingCount { get; set; } + public bool SkipMissingDate { get; set; } + public bool SkipMissingIsbn { get; set; } + public bool SkipPartsAndSets { get; set; } + public bool SkipSeriesSecondary { get; set; } + public string AllowedLanguages { get; set; } } public static class MetadataProfileResourceMapper @@ -44,51 +30,13 @@ namespace Readarr.Api.V1.Profiles.Metadata { Id = model.Id, Name = model.Name, - PrimaryAlbumTypes = model.PrimaryAlbumTypes.ConvertAll(ToResource), - SecondaryAlbumTypes = model.SecondaryAlbumTypes.ConvertAll(ToResource), - ReleaseStatuses = model.ReleaseStatuses.ConvertAll(ToResource) - }; - } - - public static ProfilePrimaryAlbumTypeItemResource ToResource(this ProfilePrimaryAlbumTypeItem model) - { - if (model == null) - { - return null; - } - - return new ProfilePrimaryAlbumTypeItemResource - { - AlbumType = model.PrimaryAlbumType, - Allowed = model.Allowed - }; - } - - public static ProfileSecondaryAlbumTypeItemResource ToResource(this ProfileSecondaryAlbumTypeItem model) - { - if (model == null) - { - return null; - } - - return new ProfileSecondaryAlbumTypeItemResource - { - AlbumType = model.SecondaryAlbumType, - Allowed = model.Allowed - }; - } - - public static ProfileReleaseStatusItemResource ToResource(this ProfileReleaseStatusItem model) - { - if (model == null) - { - return null; - } - - return new ProfileReleaseStatusItemResource - { - ReleaseStatus = model.ReleaseStatus, - Allowed = model.Allowed + MinRating = model.MinRating, + MinRatingCount = model.MinRatingCount, + SkipMissingDate = model.SkipMissingDate, + SkipMissingIsbn = model.SkipMissingIsbn, + SkipPartsAndSets = model.SkipPartsAndSets, + SkipSeriesSecondary = model.SkipSeriesSecondary, + AllowedLanguages = model.AllowedLanguages }; } @@ -103,51 +51,13 @@ namespace Readarr.Api.V1.Profiles.Metadata { Id = resource.Id, Name = resource.Name, - PrimaryAlbumTypes = resource.PrimaryAlbumTypes.ConvertAll(ToModel), - SecondaryAlbumTypes = resource.SecondaryAlbumTypes.ConvertAll(ToModel), - ReleaseStatuses = resource.ReleaseStatuses.ConvertAll(ToModel) - }; - } - - public static ProfilePrimaryAlbumTypeItem ToModel(this ProfilePrimaryAlbumTypeItemResource resource) - { - if (resource == null) - { - return null; - } - - return new ProfilePrimaryAlbumTypeItem - { - PrimaryAlbumType = (NzbDrone.Core.Music.PrimaryAlbumType)resource.AlbumType.Id, - Allowed = resource.Allowed - }; - } - - public static ProfileSecondaryAlbumTypeItem ToModel(this ProfileSecondaryAlbumTypeItemResource resource) - { - if (resource == null) - { - return null; - } - - return new ProfileSecondaryAlbumTypeItem - { - SecondaryAlbumType = (NzbDrone.Core.Music.SecondaryAlbumType)resource.AlbumType.Id, - Allowed = resource.Allowed - }; - } - - public static ProfileReleaseStatusItem ToModel(this ProfileReleaseStatusItemResource resource) - { - if (resource == null) - { - return null; - } - - return new ProfileReleaseStatusItem - { - ReleaseStatus = (NzbDrone.Core.Music.ReleaseStatus)resource.ReleaseStatus.Id, - Allowed = resource.Allowed + MinRating = resource.MinRating, + MinRatingCount = resource.MinRatingCount, + SkipMissingDate = resource.SkipMissingDate, + SkipMissingIsbn = resource.SkipMissingIsbn, + SkipPartsAndSets = resource.SkipPartsAndSets, + SkipSeriesSecondary = resource.SkipSeriesSecondary, + AllowedLanguages = resource.AllowedLanguages }; } diff --git a/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaModule.cs b/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaModule.cs index 5a66b0961..d9fb24261 100644 --- a/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaModule.cs +++ b/src/Readarr.Api.V1/Profiles/Metadata/MetadataProfileSchemaModule.cs @@ -1,4 +1,3 @@ -using System.Linq; using NzbDrone.Core.Profiles.Metadata; using Readarr.Http; @@ -14,35 +13,9 @@ namespace Readarr.Api.V1.Profiles.Metadata private MetadataProfileResource GetAll() { - var orderedPrimTypes = NzbDrone.Core.Music.PrimaryAlbumType.All - .OrderByDescending(l => l.Id) - .ToList(); - - var orderedSecTypes = NzbDrone.Core.Music.SecondaryAlbumType.All - .OrderByDescending(l => l.Id) - .ToList(); - - var orderedRelStatuses = NzbDrone.Core.Music.ReleaseStatus.All - .OrderByDescending(l => l.Id) - .ToList(); - - var primTypes = orderedPrimTypes - .Select(v => new ProfilePrimaryAlbumTypeItem { PrimaryAlbumType = v, Allowed = false }) - .ToList(); - - var secTypes = orderedSecTypes - .Select(v => new ProfileSecondaryAlbumTypeItem { SecondaryAlbumType = v, Allowed = false }) - .ToList(); - - var relStatuses = orderedRelStatuses - .Select(v => new ProfileReleaseStatusItem { ReleaseStatus = v, Allowed = false }) - .ToList(); - var profile = new MetadataProfile { - PrimaryAlbumTypes = primTypes, - SecondaryAlbumTypes = secTypes, - ReleaseStatuses = relStatuses + AllowedLanguages = "eng, en-US, en-GB" }; return profile.ToResource(); diff --git a/src/Readarr.Api.V1/Profiles/Metadata/MetadataValidator.cs b/src/Readarr.Api.V1/Profiles/Metadata/MetadataValidator.cs deleted file mode 100644 index df3607dcf..000000000 --- a/src/Readarr.Api.V1/Profiles/Metadata/MetadataValidator.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation; -using FluentValidation.Validators; - -namespace Readarr.Api.V1.Profiles.Metadata -{ - public static class MetadataValidation - { - public static IRuleBuilderOptions<T, IList<ProfilePrimaryAlbumTypeItemResource>> MustHaveAllowedPrimaryType<T>(this IRuleBuilder<T, IList<ProfilePrimaryAlbumTypeItemResource>> ruleBuilder) - { - ruleBuilder.SetValidator(new NotEmptyValidator(null)); - - return ruleBuilder.SetValidator(new PrimaryTypeValidator<T>()); - } - - public static IRuleBuilderOptions<T, IList<ProfileSecondaryAlbumTypeItemResource>> MustHaveAllowedSecondaryType<T>(this IRuleBuilder<T, IList<ProfileSecondaryAlbumTypeItemResource>> ruleBuilder) - { - ruleBuilder.SetValidator(new NotEmptyValidator(null)); - - return ruleBuilder.SetValidator(new SecondaryTypeValidator<T>()); - } - - public static IRuleBuilderOptions<T, IList<ProfileReleaseStatusItemResource>> MustHaveAllowedReleaseStatus<T>(this IRuleBuilder<T, IList<ProfileReleaseStatusItemResource>> ruleBuilder) - { - ruleBuilder.SetValidator(new NotEmptyValidator(null)); - - return ruleBuilder.SetValidator(new ReleaseStatusValidator<T>()); - } - } - - public class PrimaryTypeValidator<T> : PropertyValidator - { - public PrimaryTypeValidator() - : base("Must have at least one allowed primary type") - { - } - - protected override bool IsValid(PropertyValidatorContext context) - { - var list = context.PropertyValue as IList<ProfilePrimaryAlbumTypeItemResource>; - - if (list == null) - { - return false; - } - - if (!list.Any(c => c.Allowed)) - { - return false; - } - - return true; - } - } - - public class SecondaryTypeValidator<T> : PropertyValidator - { - public SecondaryTypeValidator() - : base("Must have at least one allowed secondary type") - { - } - - protected override bool IsValid(PropertyValidatorContext context) - { - var list = context.PropertyValue as IList<ProfileSecondaryAlbumTypeItemResource>; - - if (list == null) - { - return false; - } - - if (!list.Any(c => c.Allowed)) - { - return false; - } - - return true; - } - } - - public class ReleaseStatusValidator<T> : PropertyValidator - { - public ReleaseStatusValidator() - : base("Must have at least one allowed release status") - { - } - - protected override bool IsValid(PropertyValidatorContext context) - { - var list = context.PropertyValue as IList<ProfileReleaseStatusItemResource>; - - if (list == null) - { - return false; - } - - if (!list.Any(c => c.Allowed)) - { - return false; - } - - return true; - } - } -} diff --git a/src/Readarr.Api.V1/Queue/QueueDetailsModule.cs b/src/Readarr.Api.V1/Queue/QueueDetailsModule.cs index 25ee1f8f6..eb3387e4d 100644 --- a/src/Readarr.Api.V1/Queue/QueueDetailsModule.cs +++ b/src/Readarr.Api.V1/Queue/QueueDetailsModule.cs @@ -33,23 +33,23 @@ namespace Readarr.Api.V1.Queue var pending = _pendingReleaseService.GetPendingQueue(); var fullQueue = queue.Concat(pending); - var artistIdQuery = Request.Query.ArtistId; - var albumIdsQuery = Request.Query.AlbumIds; + var authorIdQuery = Request.Query.AuthorId; + var bookIdsQuery = Request.Query.BookIds; - if (artistIdQuery.HasValue) + if (authorIdQuery.HasValue) { - return fullQueue.Where(q => q.Artist?.Id == (int)artistIdQuery).ToResource(includeArtist, includeAlbum); + return fullQueue.Where(q => q.Artist?.Id == (int)authorIdQuery).ToResource(includeArtist, includeAlbum); } - if (albumIdsQuery.HasValue) + if (bookIdsQuery.HasValue) { - string albumIdsValue = albumIdsQuery.Value.ToString(); + string bookIdsValue = bookIdsQuery.Value.ToString(); - var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + var bookIds = bookIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(e => Convert.ToInt32(e)) .ToList(); - return fullQueue.Where(q => q.Album != null && albumIds.Contains(q.Album.Id)).ToResource(includeArtist, includeAlbum); + return fullQueue.Where(q => q.Album != null && bookIds.Contains(q.Album.Id)).ToResource(includeArtist, includeAlbum); } return fullQueue.ToResource(includeArtist, includeAlbum); diff --git a/src/Readarr.Api.V1/Queue/QueueModule.cs b/src/Readarr.Api.V1/Queue/QueueModule.cs index f2fee6fe7..74b6bab47 100644 --- a/src/Readarr.Api.V1/Queue/QueueModule.cs +++ b/src/Readarr.Api.V1/Queue/QueueModule.cs @@ -118,13 +118,13 @@ namespace Readarr.Api.V1.Queue { case "status": return q => q.Status; - case "artist.sortName": + case "authors.sortName": return q => q.Artist?.SortName; case "title": return q => q.Title; case "album": return q => q.Album; - case "album.title": + case "books.title": return q => q.Album?.Title; case "album.releaseDate": return q => q.Album?.ReleaseDate; diff --git a/src/Readarr.Api.V1/Queue/QueueResource.cs b/src/Readarr.Api.V1/Queue/QueueResource.cs index 94590a3d4..0164c4858 100644 --- a/src/Readarr.Api.V1/Queue/QueueResource.cs +++ b/src/Readarr.Api.V1/Queue/QueueResource.cs @@ -13,8 +13,8 @@ namespace Readarr.Api.V1.Queue { public class QueueResource : RestResource { - public int? ArtistId { get; set; } - public int? AlbumId { get; set; } + public int? AuthorId { get; set; } + public int? BookId { get; set; } public ArtistResource Artist { get; set; } public AlbumResource Album { get; set; } public QualityModel Quality { get; set; } @@ -48,8 +48,8 @@ namespace Readarr.Api.V1.Queue return new QueueResource { Id = model.Id, - ArtistId = model.Artist?.Id, - AlbumId = model.Album?.Id, + AuthorId = model.Artist?.Id, + BookId = model.Album?.Id, Artist = includeArtist && model.Artist != null ? model.Artist.ToResource() : null, Album = includeAlbum && model.Album != null ? model.Album.ToResource() : null, Quality = model.Quality, diff --git a/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs b/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs index a1bfaea7e..808d16eba 100644 --- a/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs +++ b/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs @@ -1,5 +1,9 @@ +using System; using System.Collections.Generic; +using System.Linq; using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Books.Calibre; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -53,6 +57,14 @@ namespace Readarr.Api.V1.RootFolders SharedValidator.RuleFor(c => c.DefaultQualityProfileId) .SetValidator(qualityProfileExistsValidator); + + SharedValidator.RuleFor(c => c.Host).ValidHost().When(x => x.IsCalibreLibrary); + SharedValidator.RuleFor(c => c.Port).InclusiveBetween(1, 65535).When(x => x.IsCalibreLibrary); + SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace()); + SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Password)); + SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username)); + + SharedValidator.RuleFor(c => c.OutputFormat).Must(x => x.Split(',').All(y => Enum.TryParse<CalibreFormat>(y, true, out _))).When(x => x.OutputFormat.IsNotNullOrWhiteSpace()).WithMessage("Invalid output formats"); } private RootFolderResource GetRootFolder(int id) diff --git a/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs b/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs index fa771a931..3bcd07423 100644 --- a/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs +++ b/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Books.Calibre; using NzbDrone.Core.Music; using NzbDrone.Core.RootFolders; using Readarr.Http.REST; @@ -14,6 +15,15 @@ namespace Readarr.Api.V1.RootFolders public int DefaultQualityProfileId { get; set; } public MonitorTypes DefaultMonitorOption { get; set; } public HashSet<int> DefaultTags { get; set; } + public bool IsCalibreLibrary { get; set; } + public string Host { get; set; } + public int Port { get; set; } + public string UrlBase { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string OutputFormat { get; set; } + public int OutputProfile { get; set; } + public bool UseSsl { get; set; } public bool Accessible { get; set; } public long? FreeSpace { get; set; } @@ -39,6 +49,15 @@ namespace Readarr.Api.V1.RootFolders DefaultQualityProfileId = model.DefaultQualityProfileId, DefaultMonitorOption = model.DefaultMonitorOption, DefaultTags = model.DefaultTags, + IsCalibreLibrary = model.IsCalibreLibrary, + Host = model.CalibreSettings?.Host, + Port = model.CalibreSettings?.Port ?? 0, + UrlBase = model.CalibreSettings?.UrlBase, + Username = model.CalibreSettings?.Username, + Password = model.CalibreSettings?.Password, + OutputFormat = model.CalibreSettings?.OutputFormat, + OutputProfile = model.CalibreSettings?.OutputProfile ?? 0, + UseSsl = model.CalibreSettings?.UseSsl ?? false, Accessible = model.Accessible, FreeSpace = model.FreeSpace, @@ -53,6 +72,26 @@ namespace Readarr.Api.V1.RootFolders return null; } + CalibreSettings cs; + if (resource.IsCalibreLibrary) + { + cs = new CalibreSettings + { + Host = resource.Host, + Port = resource.Port, + UrlBase = resource.UrlBase, + Username = resource.Username, + Password = resource.Password, + OutputFormat = resource.OutputFormat, + OutputProfile = resource.OutputProfile, + UseSsl = resource.UseSsl + }; + } + else + { + cs = null; + } + return new RootFolder { Id = resource.Id, @@ -62,7 +101,9 @@ namespace Readarr.Api.V1.RootFolders DefaultMetadataProfileId = resource.DefaultMetadataProfileId, DefaultQualityProfileId = resource.DefaultQualityProfileId, DefaultMonitorOption = resource.DefaultMonitorOption, - DefaultTags = resource.DefaultTags + DefaultTags = resource.DefaultTags, + IsCalibreLibrary = resource.IsCalibreLibrary, + CalibreSettings = cs }; } diff --git a/src/Readarr.Api.V1/Search/SearchModule.cs b/src/Readarr.Api.V1/Search/SearchModule.cs index e8565dc1a..6b987c203 100644 --- a/src/Readarr.Api.V1/Search/SearchModule.cs +++ b/src/Readarr.Api.V1/Search/SearchModule.cs @@ -35,11 +35,11 @@ namespace Readarr.Api.V1.Search var resource = new SearchResource(); resource.Id = id++; - if (result is NzbDrone.Core.Music.Artist) + if (result is NzbDrone.Core.Music.Author) { - var artist = (NzbDrone.Core.Music.Artist)result; + var artist = (NzbDrone.Core.Music.Author)result; resource.Artist = artist.ToResource(); - resource.ForeignId = artist.ForeignArtistId; + resource.ForeignId = artist.ForeignAuthorId; var poster = artist.Metadata.Value.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); if (poster != null) @@ -47,11 +47,11 @@ namespace Readarr.Api.V1.Search resource.Artist.RemotePoster = poster.Url; } } - else if (result is NzbDrone.Core.Music.Album) + else if (result is NzbDrone.Core.Music.Book) { - var album = (NzbDrone.Core.Music.Album)result; + var album = (NzbDrone.Core.Music.Book)result; resource.Album = album.ToResource(); - resource.ForeignId = album.ForeignAlbumId; + resource.ForeignId = album.ForeignBookId; var cover = album.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Cover); if (cover != null) diff --git a/src/Readarr.Api.V1/Series/SeriesBookLinkResource.cs b/src/Readarr.Api.V1/Series/SeriesBookLinkResource.cs new file mode 100644 index 000000000..c5ca540bf --- /dev/null +++ b/src/Readarr.Api.V1/Series/SeriesBookLinkResource.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Music; +using Readarr.Http.REST; + +namespace Readarr.Api.V1.Series +{ + public class SeriesBookLinkResource : RestResource + { + public string Position { get; set; } + public int SeriesId { get; set; } + public int BookId { get; set; } + } + + public static class SeriesBookLinkResourceMapper + { + public static SeriesBookLinkResource ToResource(this SeriesBookLink model) + { + return new SeriesBookLinkResource + { + Id = model.Id, + Position = model.Position, + SeriesId = model.SeriesId, + BookId = model.BookId + }; + } + + public static List<SeriesBookLinkResource> ToResource(this IEnumerable<SeriesBookLink> models) + { + return models?.Select(ToResource).ToList(); + } + } +} diff --git a/src/Readarr.Api.V1/Series/SeriesModule.cs b/src/Readarr.Api.V1/Series/SeriesModule.cs new file mode 100644 index 000000000..bb6b14350 --- /dev/null +++ b/src/Readarr.Api.V1/Series/SeriesModule.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Nancy; +using NzbDrone.Core.Music; +using Readarr.Http; +using Readarr.Http.REST; + +namespace Readarr.Api.V1.Series +{ + public class SeriesModule : ReadarrRestModule<SeriesResource> + { + protected readonly ISeriesService _seriesService; + + public SeriesModule(ISeriesService seriesService) + { + _seriesService = seriesService; + + GetResourceAll = GetSeries; + } + + private List<SeriesResource> GetSeries() + { + var authorIdQuery = Request.Query.AuthorId; + + if (!authorIdQuery.HasValue) + { + throw new BadRequestException("authorId must be provided"); + } + + int authorId = Convert.ToInt32(authorIdQuery.Value); + + return _seriesService.GetByAuthorId(authorId).ToResource(); + } + } +} diff --git a/src/Readarr.Api.V1/Series/SeriesResource.cs b/src/Readarr.Api.V1/Series/SeriesResource.cs new file mode 100644 index 000000000..5637dd919 --- /dev/null +++ b/src/Readarr.Api.V1/Series/SeriesResource.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using Readarr.Http.REST; + +namespace Readarr.Api.V1.Series +{ + public class SeriesResource : RestResource + { + public string Title { get; set; } + public string Description { get; set; } + public List<SeriesBookLinkResource> Links { get; set; } + } + + public static class SeriesResourceMapper + { + public static SeriesResource ToResource(this NzbDrone.Core.Music.Series model) + { + if (model == null) + { + return null; + } + + return new SeriesResource + { + Id = model.Id, + Title = model.Title, + Description = model.Description, + Links = model.LinkItems.Value.ToResource() + }; + } + + public static List<SeriesResource> ToResource(this IEnumerable<NzbDrone.Core.Music.Series> models) + { + return models?.Select(ToResource).ToList(); + } + } +} diff --git a/src/Readarr.Api.V1/Tags/TagDetailsResource.cs b/src/Readarr.Api.V1/Tags/TagDetailsResource.cs index 905b27c23..7d4447e91 100644 --- a/src/Readarr.Api.V1/Tags/TagDetailsResource.cs +++ b/src/Readarr.Api.V1/Tags/TagDetailsResource.cs @@ -12,7 +12,7 @@ namespace Readarr.Api.V1.Tags public List<int> ImportListIds { get; set; } public List<int> NotificationIds { get; set; } public List<int> RestrictionIds { get; set; } - public List<int> ArtistIds { get; set; } + public List<int> AuthorIds { get; set; } } public static class TagDetailsResourceMapper @@ -32,7 +32,7 @@ namespace Readarr.Api.V1.Tags ImportListIds = model.ImportListIds, NotificationIds = model.NotificationIds, RestrictionIds = model.RestrictionIds, - ArtistIds = model.ArtistIds + AuthorIds = model.AuthorIds }; } diff --git a/src/Readarr.Api.V1/TrackFiles/TrackFileModule.cs b/src/Readarr.Api.V1/TrackFiles/TrackFileModule.cs index 2980d5fc5..32278cb93 100644 --- a/src/Readarr.Api.V1/TrackFiles/TrackFileModule.cs +++ b/src/Readarr.Api.V1/TrackFiles/TrackFileModule.cs @@ -16,7 +16,7 @@ using HttpStatusCode = System.Net.HttpStatusCode; namespace Readarr.Api.V1.TrackFiles { - public class TrackFileModule : ReadarrRestModuleWithSignalR<TrackFileResource, TrackFile>, + public class TrackFileModule : ReadarrRestModuleWithSignalR<TrackFileResource, BookFile>, IHandle<TrackFileAddedEvent>, IHandle<TrackFileDeletedEvent> { @@ -52,9 +52,9 @@ namespace Readarr.Api.V1.TrackFiles Delete("/bulk", trackFiles => DeleteTrackFiles()); } - private TrackFileResource MapToResource(TrackFile trackFile) + private TrackFileResource MapToResource(BookFile trackFile) { - if (trackFile.AlbumId > 0 && trackFile.Artist != null && trackFile.Artist.Value != null) + if (trackFile.BookId > 0 && trackFile.Artist != null && trackFile.Artist.Value != null) { return trackFile.ToResource(trackFile.Artist.Value, _upgradableSpecification); } @@ -73,14 +73,14 @@ namespace Readarr.Api.V1.TrackFiles private List<TrackFileResource> GetTrackFiles() { - var artistIdQuery = Request.Query.ArtistId; + var authorIdQuery = Request.Query.AuthorId; var trackFileIdsQuery = Request.Query.TrackFileIds; - var albumIdQuery = Request.Query.AlbumId; + var bookIdQuery = Request.Query.BookId; var unmappedQuery = Request.Query.Unmapped; - if (!artistIdQuery.HasValue && !trackFileIdsQuery.HasValue && !albumIdQuery.HasValue && !unmappedQuery.HasValue) + if (!authorIdQuery.HasValue && !trackFileIdsQuery.HasValue && !bookIdQuery.HasValue && !unmappedQuery.HasValue) { - throw new Readarr.Http.REST.BadRequestException("artistId, albumId, trackFileIds or unmapped must be provided"); + throw new Readarr.Http.REST.BadRequestException("authorId, bookId, trackFileIds or unmapped must be provided"); } if (unmappedQuery.HasValue && Convert.ToBoolean(unmappedQuery.Value)) @@ -89,27 +89,27 @@ namespace Readarr.Api.V1.TrackFiles return files.ConvertAll(f => MapToResource(f)); } - if (artistIdQuery.HasValue && !albumIdQuery.HasValue) + if (authorIdQuery.HasValue && !bookIdQuery.HasValue) { - int artistId = Convert.ToInt32(artistIdQuery.Value); - var artist = _artistService.GetArtist(artistId); + int authorId = Convert.ToInt32(authorIdQuery.Value); + var artist = _artistService.GetArtist(authorId); - return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _upgradableSpecification)); + return _mediaFileService.GetFilesByArtist(authorId).ConvertAll(f => f.ToResource(artist, _upgradableSpecification)); } - if (albumIdQuery.HasValue) + if (bookIdQuery.HasValue) { - string albumIdValue = albumIdQuery.Value.ToString(); + string bookIdValue = bookIdQuery.Value.ToString(); - var albumIds = albumIdValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + var bookIds = bookIdValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(e => Convert.ToInt32(e)) .ToList(); var result = new List<TrackFileResource>(); - foreach (var albumId in albumIds) + foreach (var bookId in bookIds) { - var album = _albumService.GetAlbum(albumId); - var albumArtist = _artistService.GetArtist(album.ArtistId); + var album = _albumService.GetAlbum(bookId); + var albumArtist = _artistService.GetArtist(album.AuthorId); result.AddRange(_mediaFileService.GetFilesByAlbum(album.Id).ConvertAll(f => f.ToResource(albumArtist, _upgradableSpecification))); } @@ -164,7 +164,7 @@ namespace Readarr.Api.V1.TrackFiles throw new NzbDroneClientException(HttpStatusCode.NotFound, "Track file not found"); } - if (trackFile.AlbumId > 0 && trackFile.Artist != null && trackFile.Artist.Value != null) + if (trackFile.BookId > 0 && trackFile.Artist != null && trackFile.Artist.Value != null) { _mediaFileDeletionService.DeleteTrackFile(trackFile.Artist.Value, trackFile); } diff --git a/src/Readarr.Api.V1/TrackFiles/TrackFileResource.cs b/src/Readarr.Api.V1/TrackFiles/TrackFileResource.cs index fc249fdbb..7545af5c1 100644 --- a/src/Readarr.Api.V1/TrackFiles/TrackFileResource.cs +++ b/src/Readarr.Api.V1/TrackFiles/TrackFileResource.cs @@ -11,8 +11,8 @@ namespace Readarr.Api.V1.TrackFiles { public class TrackFileResource : RestResource { - public int ArtistId { get; set; } - public int AlbumId { get; set; } + public int AuthorId { get; set; } + public int BookId { get; set; } public string Path { get; set; } public long Size { get; set; } public DateTime DateAdded { get; set; } @@ -39,7 +39,7 @@ namespace Readarr.Api.V1.TrackFiles return qualityWeight; } - public static TrackFileResource ToResource(this TrackFile model) + public static TrackFileResource ToResource(this BookFile model) { if (model == null) { @@ -49,7 +49,7 @@ namespace Readarr.Api.V1.TrackFiles return new TrackFileResource { Id = model.Id, - AlbumId = model.AlbumId, + BookId = model.BookId, Path = model.Path, Size = model.Size, DateAdded = model.DateAdded, @@ -59,7 +59,7 @@ namespace Readarr.Api.V1.TrackFiles }; } - public static TrackFileResource ToResource(this TrackFile model, NzbDrone.Core.Music.Artist artist, IUpgradableSpecification upgradableSpecification) + public static TrackFileResource ToResource(this BookFile model, NzbDrone.Core.Music.Author artist, IUpgradableSpecification upgradableSpecification) { if (model == null) { @@ -70,8 +70,8 @@ namespace Readarr.Api.V1.TrackFiles { Id = model.Id, - ArtistId = artist.Id, - AlbumId = model.AlbumId, + AuthorId = artist.Id, + BookId = model.BookId, Path = model.Path, Size = model.Size, DateAdded = model.DateAdded, diff --git a/src/Readarr.Api.V1/Tracks/TrackModule.cs b/src/Readarr.Api.V1/Tracks/TrackModule.cs deleted file mode 100644 index 2d4fabd0c..000000000 --- a/src/Readarr.Api.V1/Tracks/TrackModule.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Nancy; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Music; -using NzbDrone.SignalR; -using Readarr.Http.REST; - -namespace Readarr.Api.V1.Tracks -{ - public class TrackModule : TrackModuleWithSignalR - { - public TrackModule(IArtistService artistService, - ITrackService trackService, - IUpgradableSpecification upgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(trackService, artistService, upgradableSpecification, signalRBroadcaster) - { - GetResourceAll = GetTracks; - } - - private List<TrackResource> GetTracks() - { - var artistIdQuery = Request.Query.ArtistId; - var albumIdQuery = Request.Query.AlbumId; - var albumReleaseIdQuery = Request.Query.AlbumReleaseId; - var trackIdsQuery = Request.Query.TrackIds; - - if (!artistIdQuery.HasValue && !trackIdsQuery.HasValue && !albumIdQuery.HasValue && !albumReleaseIdQuery.HasValue) - { - throw new BadRequestException("One of artistId, albumId, albumReleaseId or trackIds must be provided"); - } - - if (artistIdQuery.HasValue && !albumIdQuery.HasValue) - { - int artistId = Convert.ToInt32(artistIdQuery.Value); - - return MapToResource(_trackService.GetTracksByArtist(artistId), false, false); - } - - if (albumReleaseIdQuery.HasValue) - { - int releaseId = Convert.ToInt32(albumReleaseIdQuery.Value); - - return MapToResource(_trackService.GetTracksByRelease(releaseId), false, false); - } - - if (albumIdQuery.HasValue) - { - int albumId = Convert.ToInt32(albumIdQuery.Value); - - return MapToResource(_trackService.GetTracksByAlbum(albumId), false, false); - } - - string trackIdsValue = trackIdsQuery.Value.ToString(); - - var trackIds = trackIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => Convert.ToInt32(e)) - .ToList(); - - return MapToResource(_trackService.GetTracks(trackIds), false, false); - } - } -} diff --git a/src/Readarr.Api.V1/Tracks/TrackModuleWithSignalR.cs b/src/Readarr.Api.V1/Tracks/TrackModuleWithSignalR.cs deleted file mode 100644 index a904c1bc8..000000000 --- a/src/Readarr.Api.V1/Tracks/TrackModuleWithSignalR.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Music; -using NzbDrone.SignalR; -using Readarr.Api.V1.Artist; -using Readarr.Api.V1.TrackFiles; -using Readarr.Http; - -namespace Readarr.Api.V1.Tracks -{ - public abstract class TrackModuleWithSignalR : ReadarrRestModuleWithSignalR<TrackResource, Track>, - IHandle<TrackImportedEvent>, - IHandle<TrackFileDeletedEvent> - { - protected readonly ITrackService _trackService; - protected readonly IArtistService _artistService; - protected readonly IUpgradableSpecification _upgradableSpecification; - - protected TrackModuleWithSignalR(ITrackService trackService, - IArtistService artistService, - IUpgradableSpecification upgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(signalRBroadcaster) - { - _trackService = trackService; - _artistService = artistService; - _upgradableSpecification = upgradableSpecification; - - GetResourceById = GetTrack; - } - - protected TrackModuleWithSignalR(ITrackService trackService, - IArtistService artistService, - IUpgradableSpecification upgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster, - string resource) - : base(signalRBroadcaster, resource) - { - _trackService = trackService; - _artistService = artistService; - _upgradableSpecification = upgradableSpecification; - - GetResourceById = GetTrack; - } - - protected TrackResource GetTrack(int id) - { - var track = _trackService.GetTrack(id); - var resource = MapToResource(track, true, true); - return resource; - } - - protected TrackResource MapToResource(Track track, bool includeArtist, bool includeTrackFile) - { - var resource = track.ToResource(); - - if (includeArtist || includeTrackFile) - { - var artist = track.Artist.Value; - - if (includeArtist) - { - resource.Artist = artist.ToResource(); - } - - if (includeTrackFile && track.TrackFileId != 0) - { - resource.TrackFile = track.TrackFile.Value.ToResource(artist, _upgradableSpecification); - } - } - - return resource; - } - - protected List<TrackResource> MapToResource(List<Track> tracks, bool includeArtist, bool includeTrackFile) - { - var result = tracks.ToResource(); - - if (includeArtist || includeTrackFile) - { - var artistDict = new Dictionary<int, NzbDrone.Core.Music.Artist>(); - for (var i = 0; i < tracks.Count; i++) - { - var track = tracks[i]; - var resource = result[i]; - var artist = track.Artist.Value; - - if (includeArtist) - { - resource.Artist = artist.ToResource(); - } - - if (includeTrackFile && tracks[i].TrackFileId != 0) - { - resource.TrackFile = tracks[i].TrackFile.Value.ToResource(artist, _upgradableSpecification); - } - } - } - - return result; - } - - public void Handle(TrackImportedEvent message) - { - foreach (var track in message.TrackInfo.Tracks) - { - track.TrackFile = message.ImportedTrack; - BroadcastResourceChange(ModelAction.Updated, MapToResource(track, true, true)); - } - } - - public void Handle(TrackFileDeletedEvent message) - { - foreach (var track in message.TrackFile.Tracks.Value) - { - track.TrackFile = message.TrackFile; - BroadcastResourceChange(ModelAction.Updated, MapToResource(track, true, true)); - } - } - } -} diff --git a/src/Readarr.Api.V1/Tracks/TrackResource.cs b/src/Readarr.Api.V1/Tracks/TrackResource.cs deleted file mode 100644 index 948cd77e4..000000000 --- a/src/Readarr.Api.V1/Tracks/TrackResource.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using NzbDrone.Core.Music; -using Readarr.Api.V1.Artist; -using Readarr.Api.V1.TrackFiles; -using Readarr.Http.REST; - -namespace Readarr.Api.V1.Tracks -{ - public class TrackResource : RestResource - { - public int ArtistId { get; set; } - public int TrackFileId { get; set; } - public int AlbumId { get; set; } - public bool Explicit { get; set; } - public int AbsoluteTrackNumber { get; set; } - public string TrackNumber { get; set; } - public string Title { get; set; } - public int Duration { get; set; } - public TrackFileResource TrackFile { get; set; } - public int MediumNumber { get; set; } - public bool HasFile { get; set; } - - public ArtistResource Artist { get; set; } - public Ratings Ratings { get; set; } - - //Hiding this so people don't think its usable (only used to set the initial state) - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool Grabbed { get; set; } - } - - public static class TrackResourceMapper - { - public static TrackResource ToResource(this Track model) - { - if (model == null) - { - return null; - } - - return new TrackResource - { - Id = model.Id, - - ArtistId = model.Artist.Value.Id, - TrackFileId = model.TrackFileId, - AlbumId = model.AlbumId, - Explicit = model.Explicit, - AbsoluteTrackNumber = model.AbsoluteTrackNumber, - TrackNumber = model.TrackNumber, - Title = model.Title, - Duration = model.Duration, - MediumNumber = model.MediumNumber, - HasFile = model.HasFile, - Ratings = model.Ratings, - }; - } - - public static List<TrackResource> ToResource(this IEnumerable<Track> models) - { - if (models == null) - { - return null; - } - - return models.Select(ToResource).ToList(); - } - } -} diff --git a/src/Readarr.Api.V1/Wanted/CutoffModule.cs b/src/Readarr.Api.V1/Wanted/CutoffModule.cs index 23e61724c..91d9656af 100644 --- a/src/Readarr.Api.V1/Wanted/CutoffModule.cs +++ b/src/Readarr.Api.V1/Wanted/CutoffModule.cs @@ -29,7 +29,7 @@ namespace Readarr.Api.V1.Wanted private PagingResource<AlbumResource> GetCutoffUnmetAlbums(PagingResource<AlbumResource> pagingResource) { - var pagingSpec = new PagingSpec<Album> + var pagingSpec = new PagingSpec<Book> { Page = pagingResource.Page, PageSize = pagingResource.PageSize, @@ -42,11 +42,11 @@ namespace Readarr.Api.V1.Wanted if (filter != null && filter.Value == "false") { - pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Artist.Value.Monitored == false); + pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Author.Value.Monitored == false); } else { - pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true); + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Author.Value.Monitored == true); } var resource = ApplyToPage(_albumCutoffService.AlbumsWhereCutoffUnmet, pagingSpec, v => MapToResource(v, includeArtist)); diff --git a/src/Readarr.Api.V1/Wanted/MissingModule.cs b/src/Readarr.Api.V1/Wanted/MissingModule.cs index 6ddd31f80..41d931744 100644 --- a/src/Readarr.Api.V1/Wanted/MissingModule.cs +++ b/src/Readarr.Api.V1/Wanted/MissingModule.cs @@ -25,7 +25,7 @@ namespace Readarr.Api.V1.Wanted private PagingResource<AlbumResource> GetMissingAlbums(PagingResource<AlbumResource> pagingResource) { - var pagingSpec = new PagingSpec<Album> + var pagingSpec = new PagingSpec<Book> { Page = pagingResource.Page, PageSize = pagingResource.PageSize, @@ -38,11 +38,11 @@ namespace Readarr.Api.V1.Wanted if (monitoredFilter != null && monitoredFilter.Value == "false") { - pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Artist.Value.Monitored == false); + pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Author.Value.Monitored == false); } else { - pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Artist.Value.Monitored == true); + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Author.Value.Monitored == true); } var resource = ApplyToPage(_albumService.AlbumsWithoutFiles, pagingSpec, v => MapToResource(v, includeArtist)); diff --git a/yarn.lock b/yarn.lock index 9c094ecd0..6b9ed1975 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,34 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@7.5.5", "@babel/core@>=7.2.2": +"@babel/code-frame@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== + dependencies: + "@babel/highlight" "^7.8.3" + +"@babel/core@7.7.5": + version "7.7.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.5.tgz#ae1323cd035b5160293307f50647e83f8ba62f7e" + integrity sha512-M42+ScN4+1S9iB6f+TL7QBpoQETxbclx+KNoKJABghnKYE+fMzSGqst0BZJc8CpI625bwPwYgUyRvxZ+0mZzpw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.7.4" + "@babel/helpers" "^7.7.4" + "@babel/parser" "^7.7.5" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@babel/types" "^7.7.4" + convert-source-map "^1.7.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/core@>=7.2.2": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg== @@ -40,66 +67,85 @@ source-map "^0.5.0" trim-right "^1.0.1" -"@babel/helper-annotate-as-pure@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" - integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q== - dependencies: - "@babel/types" "^7.0.0" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz#6b69628dfe4087798e0c4ed98e3d4a6b2fbd2f5f" - integrity sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w== +"@babel/generator@^7.7.4", "@babel/generator@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.6.tgz#57adf96d370c9a63c241cd719f9111468578537a" + integrity sha512-4bpOR5ZBz+wWcMeVtcf7FbjcFzCp+817z2/gHNncIRcM9MmKzUhtWCYAq27RAfUrAFwb+OCG1s9WEaVxfi6cjg== dependencies: - "@babel/helper-explode-assignable-expression" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/types" "^7.8.6" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" -"@babel/helper-builder-react-jsx@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz#a1ac95a5d2b3e88ae5e54846bf462eeb81b318a4" - integrity sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw== +"@babel/helper-annotate-as-pure@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" + integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== dependencies: - "@babel/types" "^7.3.0" - esutils "^2.0.0" + "@babel/types" "^7.8.3" -"@babel/helper-call-delegate@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz#87c1f8ca19ad552a736a7a27b1c1fcf8b1ff1f43" - integrity sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz#c84097a427a061ac56a1c30ebf54b7b22d241503" + integrity sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw== dependencies: - "@babel/helper-hoist-variables" "^7.4.4" - "@babel/traverse" "^7.4.4" - "@babel/types" "^7.4.4" + "@babel/helper-explode-assignable-expression" "^7.8.3" + "@babel/types" "^7.8.3" -"@babel/helper-create-class-features-plugin@^7.4.4", "@babel/helper-create-class-features-plugin@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz#401f302c8ddbc0edd36f7c6b2887d8fa1122e5a4" - integrity sha512-ZsxkyYiRA7Bg+ZTRpPvB6AbOFKTFFK4LrvTet8lInm0V468MWCaSYJE+I7v2z2r8KNLtYiV+K5kTCnR7dvyZjg== +"@babel/helper-builder-react-jsx@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.8.3.tgz#dee98d7d79cc1f003d80b76fe01c7f8945665ff6" + integrity sha512-JT8mfnpTkKNCboTqZsQTdGo3l3Ik3l7QIt9hh0O9DYiwVel37VoJpILKM4YFbP2euF32nkQSb+F9cUk9b7DDXQ== dependencies: - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-member-expression-to-functions" "^7.5.5" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.5.5" - "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/types" "^7.8.3" + esutils "^2.0.0" -"@babel/helper-define-map@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz#3dec32c2046f37e09b28c93eb0b103fd2a25d369" - integrity sha512-fTfxx7i0B5NJqvUOBBGREnrqbTxRh7zinBANpZXAVDlsZxYdclDp467G1sQ8VZYMnAURY3RpBUAgOYT9GfzHBg== - dependencies: - "@babel/helper-function-name" "^7.1.0" - "@babel/types" "^7.5.5" +"@babel/helper-call-delegate@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.8.3.tgz#de82619898aa605d409c42be6ffb8d7204579692" + integrity sha512-6Q05px0Eb+N4/GTyKPPvnkig7Lylw+QzihMpws9iiZQv7ZImf84ZsZpQH7QoWN4n4tm81SnSzPgHw2qtO0Zf3A== + dependencies: + "@babel/helper-hoist-variables" "^7.8.3" + "@babel/traverse" "^7.8.3" + "@babel/types" "^7.8.3" + +"@babel/helper-create-class-features-plugin@^7.7.4": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.6.tgz#243a5b46e2f8f0f674dc1387631eb6b28b851de0" + integrity sha512-klTBDdsr+VFFqaDHm5rR69OpEQtO2Qv8ECxHS1mNhJJvaHArR6a1xTf5K/eZW7eZpJbhCx3NW1Yt/sKsLXLblg== + dependencies: + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-member-expression-to-functions" "^7.8.3" + "@babel/helper-optimise-call-expression" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.6" + "@babel/helper-split-export-declaration" "^7.8.3" + +"@babel/helper-create-regexp-features-plugin@^7.8.3": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.6.tgz#7fa040c97fb8aebe1247a5c645330c32d083066b" + integrity sha512-bPyujWfsHhV/ztUkwGHz/RPV1T1TDEsSZDsN42JPehndA+p1KKTh3npvTadux0ZhCrytx9tvjpWNowKby3tM6A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/helper-regex" "^7.8.3" + regexpu-core "^4.6.0" + +"@babel/helper-define-map@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15" + integrity sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g== + dependencies: + "@babel/helper-function-name" "^7.8.3" + "@babel/types" "^7.8.3" lodash "^4.17.13" -"@babel/helper-explode-assignable-expression@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz#537fa13f6f1674df745b0c00ec8fe4e99681c8f6" - integrity sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA== +"@babel/helper-explode-assignable-expression@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz#a728dc5b4e89e30fc2dfc7d04fa28a930653f982" + integrity sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw== dependencies: - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/traverse" "^7.8.3" + "@babel/types" "^7.8.3" "@babel/helper-function-name@^7.1.0": version "7.1.0" @@ -110,6 +156,15 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-function-name@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" + integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA== + dependencies: + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/types" "^7.8.3" + "@babel/helper-get-function-arity@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" @@ -117,86 +172,99 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helper-hoist-variables@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a" - integrity sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w== +"@babel/helper-get-function-arity@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" + integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== dependencies: - "@babel/types" "^7.4.4" + "@babel/types" "^7.8.3" -"@babel/helper-member-expression-to-functions@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz#1fb5b8ec4453a93c439ee9fe3aeea4a84b76b590" - integrity sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA== +"@babel/helper-hoist-variables@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz#1dbe9b6b55d78c9b4183fc8cdc6e30ceb83b7134" + integrity sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg== dependencies: - "@babel/types" "^7.5.5" + "@babel/types" "^7.8.3" -"@babel/helper-module-imports@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d" - integrity sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A== +"@babel/helper-member-expression-to-functions@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" + integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.8.3" -"@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a" - integrity sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw== +"@babel/helper-module-imports@^7.7.4", "@babel/helper-module-imports@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498" + integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg== dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/helper-simple-access" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/template" "^7.4.4" - "@babel/types" "^7.5.5" + "@babel/types" "^7.8.3" + +"@babel/helper-module-transforms@^7.8.3": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.8.6.tgz#6a13b5eecadc35692047073a64e42977b97654a4" + integrity sha512-RDnGJSR5EFBJjG3deY0NiL0K9TO8SXxS9n/MPsbPK/s9LbQymuLNtlzvDiNS7IpecuL45cMeLVkA+HfmlrnkRg== + dependencies: + "@babel/helper-module-imports" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.6" + "@babel/helper-simple-access" "^7.8.3" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/template" "^7.8.6" + "@babel/types" "^7.8.6" lodash "^4.17.13" -"@babel/helper-optimise-call-expression@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz#a2920c5702b073c15de51106200aa8cad20497d5" - integrity sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g== +"@babel/helper-optimise-call-expression@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9" + integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.8.3" "@babel/helper-plugin-utils@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== -"@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351" - integrity sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw== +"@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" + integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== + +"@babel/helper-regex@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" + integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== dependencies: lodash "^4.17.13" -"@babel/helper-remap-async-to-generator@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f" - integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg== +"@babel/helper-remap-async-to-generator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz#273c600d8b9bf5006142c1e35887d555c12edd86" + integrity sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA== dependencies: - "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-wrap-function" "^7.1.0" - "@babel/template" "^7.1.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/helper-wrap-function" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.3" + "@babel/types" "^7.8.3" -"@babel/helper-replace-supers@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz#f84ce43df031222d2bad068d2626cb5799c34bc2" - integrity sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg== +"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" + integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== dependencies: - "@babel/helper-member-expression-to-functions" "^7.5.5" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.5.5" - "@babel/types" "^7.5.5" + "@babel/helper-member-expression-to-functions" "^7.8.3" + "@babel/helper-optimise-call-expression" "^7.8.3" + "@babel/traverse" "^7.8.6" + "@babel/types" "^7.8.6" -"@babel/helper-simple-access@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c" - integrity sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w== +"@babel/helper-simple-access@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae" + integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw== dependencies: - "@babel/template" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/template" "^7.8.3" + "@babel/types" "^7.8.3" "@babel/helper-split-export-declaration@^7.4.4": version "7.4.4" @@ -205,15 +273,22 @@ dependencies: "@babel/types" "^7.4.4" -"@babel/helper-wrap-function@^7.1.0", "@babel/helper-wrap-function@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" - integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ== +"@babel/helper-split-export-declaration@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" + integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== dependencies: - "@babel/helper-function-name" "^7.1.0" - "@babel/template" "^7.1.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.2.0" + "@babel/types" "^7.8.3" + +"@babel/helper-wrap-function@^7.7.4", "@babel/helper-wrap-function@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610" + integrity sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ== + dependencies: + "@babel/helper-function-name" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.3" + "@babel/types" "^7.8.3" "@babel/helpers@^7.5.5": version "7.5.5" @@ -224,6 +299,15 @@ "@babel/traverse" "^7.5.5" "@babel/types" "^7.5.5" +"@babel/helpers@^7.7.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.4.tgz#754eb3ee727c165e0a240d6c207de7c455f36f73" + integrity sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w== + dependencies: + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.4" + "@babel/types" "^7.8.3" + "@babel/highlight@^7.0.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" @@ -233,577 +317,604 @@ esutils "^2.0.2" js-tokens "^4.0.0" +"@babel/highlight@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" + integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + "@babel/parser@^7.0.0", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== -"@babel/plugin-proposal-async-generator-functions@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" - integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ== +"@babel/parser@^7.7.5", "@babel/parser@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.6.tgz#ba5c9910cddb77685a008e3c587af8d27b67962c" + integrity sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g== + +"@babel/plugin-proposal-async-generator-functions@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f" + integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-remap-async-to-generator" "^7.1.0" - "@babel/plugin-syntax-async-generators" "^7.2.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-remap-async-to-generator" "^7.8.3" + "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-class-properties@7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4" - integrity sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A== +"@babel/plugin-proposal-class-properties@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.4.tgz#2f964f0cb18b948450362742e33e15211e77c2ba" + integrity sha512-EcuXeV4Hv1X3+Q1TsuOmyyxeTRiSqurGJ26+I/FW1WbymmRRapVORm6x1Zl3iDIHyRxEs+VXWp6qnlcfcJSbbw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.5.5" + "@babel/helper-create-class-features-plugin" "^7.7.4" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-proposal-decorators@7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz#de9b2a1a8ab0196f378e2a82f10b6e2a36f21cc0" - integrity sha512-z7MpQz3XC/iQJWXH9y+MaWcLPNSMY9RQSthrLzak8R8hCj0fuyNk+Dzi9kfNe/JxxlWQ2g7wkABbgWjW36MTcw== +"@babel/plugin-proposal-decorators@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.7.4.tgz#58c1e21d21ea12f9f5f0a757e46e687b94a7ab2b" + integrity sha512-GftcVDcLCwVdzKmwOBDjATd548+IE+mBo7ttgatqNDR7VG7GqIuZPtRWlMLHbhTXhcnFZiGER8iIYl1n/imtsg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.4.4" + "@babel/helper-create-class-features-plugin" "^7.7.4" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-decorators" "^7.2.0" + "@babel/plugin-syntax-decorators" "^7.7.4" -"@babel/plugin-proposal-dynamic-import@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz#e532202db4838723691b10a67b8ce509e397c506" - integrity sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw== +"@babel/plugin-proposal-dynamic-import@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054" + integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-dynamic-import" "^7.2.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-export-default-from@7.5.2": - version "7.5.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.5.2.tgz#2c0ac2dcc36e3b2443fead2c3c5fc796fb1b5145" - integrity sha512-wr9Itk05L1/wyyZKVEmXWCdcsp/e185WUNl6AfYZeEKYaUPPvHXRDqO5K1VH7/UamYqGJowFRuCv30aDYZawsg== +"@babel/plugin-proposal-export-default-from@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.7.4.tgz#890de3c0c475374638292df31f6582160b54d639" + integrity sha512-1t6dh7BHYUz4zD1m4pozYYEZy/3m8dgOr9owx3r0mPPI3iGKRUKUbIxfYmcJ4hwljs/dhd0qOTr1ZDUp43ix+w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-export-default-from" "^7.2.0" + "@babel/plugin-syntax-export-default-from" "^7.7.4" -"@babel/plugin-proposal-export-namespace-from@7.5.2": - version "7.5.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.5.2.tgz#ccd5ed05b06d700688ff1db01a9dd27155e0d2a0" - integrity sha512-TKUdOL07anjZEbR1iSxb5WFh810KyObdd29XLFLGo1IDsSuGrjH3ouWSbAxHNmrVKzr9X71UYl2dQ7oGGcRp0g== +"@babel/plugin-proposal-export-namespace-from@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.7.4.tgz#9b32a9e3538ba4b0e2fa08942f0a8e5f60899dea" + integrity sha512-3whN5U7iZjKdbwRSFwBOjGBgH7apXCzwielljxVH8D/iYcGRqPPw63vlIbG0GqQoT9bO0QYPcIUVkhQG5hcHtg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-export-namespace-from" "^7.2.0" + "@babel/plugin-syntax-export-namespace-from" "^7.7.4" -"@babel/plugin-proposal-function-sent@7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.5.0.tgz#39233aa801145e7d8072077cdb2d25f781c1ffd7" - integrity sha512-JXdfiQpKoC6UgQliZkp3NX7K3MVec1o1nfTWiCCIORE5ag/QZXhL0aSD8/Y2K+hIHonSTxuJF9rh9zsB6hBi2A== +"@babel/plugin-proposal-function-sent@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.7.4.tgz#a1aaa820ed5210da7e31edee42f1a4cdc3ec1ba3" + integrity sha512-vCiie58siJZoGJBQT0WIKORMqCe6CFasTf2X1LOfyAiWYfLFcDCVg+Y4HIiDFH8hKwkMDGKJT6nLYHM0VmQZXA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-wrap-function" "^7.2.0" - "@babel/plugin-syntax-function-sent" "^7.2.0" + "@babel/helper-wrap-function" "^7.7.4" + "@babel/plugin-syntax-function-sent" "^7.7.4" -"@babel/plugin-proposal-json-strings@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" - integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg== +"@babel/plugin-proposal-json-strings@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b" + integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-json-strings" "^7.2.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.4.4.tgz#41c360d59481d88e0ce3a3f837df10121a769b39" - integrity sha512-Amph7Epui1Dh/xxUxS2+K22/MUi6+6JVTvy3P58tja3B6yKTSjwwx0/d83rF7551D6PVSSoplQb8GCwqec7HRw== +"@babel/plugin-proposal-nullish-coalescing-operator@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.7.4.tgz#7db302c83bc30caa89e38fee935635ef6bd11c28" + integrity sha512-TbYHmr1Gl1UC7Vo2HVuj/Naci5BEGNZ0AJhzqD2Vpr6QPFWpUmBRLrIDjedzx7/CShq0bRDS2gI4FIs77VHLVQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.2.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.7.4" -"@babel/plugin-proposal-numeric-separator@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.2.0.tgz#646854daf4cd22fd6733f6076013a936310443ac" - integrity sha512-DohMOGDrZiMKS7LthjUZNNcWl8TAf5BZDwZAH4wpm55FuJTHgfqPGdibg7rZDmont/8Yg0zA03IgT6XLeP+4sg== +"@babel/plugin-proposal-numeric-separator@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.7.4.tgz#7819a17445f4197bb9575e5750ed349776da858a" + integrity sha512-CG605v7lLpVgVldSY6kxsN9ui1DxFOyepBfuX2AzU2TNriMAYApoU55mrGw9Jr4TlrTzPCG10CL8YXyi+E/iPw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-numeric-separator" "^7.2.0" + "@babel/plugin-syntax-numeric-separator" "^7.7.4" -"@babel/plugin-proposal-object-rest-spread@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.5.tgz#61939744f71ba76a3ae46b5eea18a54c16d22e58" - integrity sha512-F2DxJJSQ7f64FyTVl5cw/9MWn6naXGdk3Q3UhDbFEEHv+EilCPoeRD3Zh/Utx1CJz4uyKlQ4uH+bJPbEhMV7Zw== +"@babel/plugin-proposal-object-rest-spread@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz#eb5ae366118ddca67bed583b53d7554cad9951bb" + integrity sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" -"@babel/plugin-proposal-optional-catch-binding@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" - integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g== +"@babel/plugin-proposal-optional-catch-binding@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9" + integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.2.0.tgz#ae454f4c21c6c2ce8cb2397dc332ae8b420c5441" - integrity sha512-ea3Q6edZC/55wEBVZAEz42v528VulyO0eir+7uky/sT4XRcdkWJcFi1aPtitTlwUzGnECWJNExWww1SStt+yWw== +"@babel/plugin-proposal-optional-chaining@7.7.5": + version "7.7.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.7.5.tgz#f0835f044cef85b31071a924010a2a390add11d4" + integrity sha512-sOwFqT8JSchtJeDD+CjmWCaiFoLxY4Ps7NjvwHC/U7l4e9i5pTRNt8nDMIFSOUL+ncFbYSwruHM8WknYItWdXw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-optional-chaining" "^7.2.0" + "@babel/plugin-syntax-optional-chaining" "^7.7.4" -"@babel/plugin-proposal-throw-expressions@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz#2d9e452d370f139000e51db65d0a85dc60c64739" - integrity sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw== +"@babel/plugin-proposal-throw-expressions@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.7.4.tgz#0321bd4acb699abef3006f7cd3d1b2c00daf1b82" + integrity sha512-yMcK1dM9Rv+Y5n62rKaHfRoRD4eOWIqYn4uy/Xu7C47rJKaR5JpQR905Hc/OL8EEaGNcEyuvjOtYdNAVXZKDZQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-throw-expressions" "^7.2.0" + "@babel/plugin-syntax-throw-expressions" "^7.7.4" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78" - integrity sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA== +"@babel/plugin-proposal-unicode-property-regex@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.3.tgz#b646c3adea5f98800c9ab45105ac34d06cd4a47f" + integrity sha512-1/1/rEZv2XGweRwwSkLpY+s60za9OZ1hJs4YDqFHCw0kYWYwL5IFljVY1MYBL+weT1l9pokDO2uhSTLVxzoHkQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.4.4" - regexpu-core "^4.5.4" + "@babel/helper-create-regexp-features-plugin" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-async-generators@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f" - integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg== +"@babel/plugin-syntax-async-generators@^7.7.4", "@babel/plugin-syntax-async-generators@^7.8.0": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-decorators@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.2.0.tgz#c50b1b957dcc69e4b1127b65e1c33eef61570c1b" - integrity sha512-38QdqVoXdHUQfTpZo3rQwqQdWtCn5tMv4uV6r2RMfTqNBuv4ZBhz79SfaQWKTVmxHjeFv/DnXVC/+agHCklYWA== +"@babel/plugin-syntax-decorators@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.8.3.tgz#8d2c15a9f1af624b0025f961682a9d53d3001bda" + integrity sha512-8Hg4dNNT9/LcA1zQlfwuKR8BUc/if7Q7NkTam9sGTcJphLwpf2g4S42uhspQrIrR+dpzE0dtTqBVFoHl8GtnnQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-dynamic-import@7.2.0", "@babel/plugin-syntax-dynamic-import@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612" - integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w== +"@babel/plugin-syntax-dynamic-import@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.7.4.tgz#29ca3b4415abfe4a5ec381e903862ad1a54c3aec" + integrity sha512-jHQW0vbRGvwQNgyVxwDh4yuXu4bH1f5/EICJLAhl1SblLs2CDhrsmCk+v5XLdE9wxtAFRyxx+P//Iw+a5L/tTg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-export-default-from@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.2.0.tgz#edd83b7adc2e0d059e2467ca96c650ab6d2f3820" - integrity sha512-c7nqUnNST97BWPtoe+Ssi+fJukc9P9/JMZ71IOMNQWza2E+Psrd46N6AEvtw6pqK+gt7ChjXyrw4SPDO79f3Lw== +"@babel/plugin-syntax-dynamic-import@^7.7.4", "@babel/plugin-syntax-dynamic-import@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-export-namespace-from@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.2.0.tgz#8d257838c6b3b779db52c0224443459bd27fb039" - integrity sha512-1zGA3UNch6A+A11nIzBVEaE3DDJbjfB+eLIcf0GGOh/BJr/8NxL3546MGhV/r0RhH4xADFIEso39TKCfEMlsGA== +"@babel/plugin-syntax-export-default-from@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.8.3.tgz#f1e55ce850091442af4ba9c2550106035b29d678" + integrity sha512-a1qnnsr73KLNIQcQlcQ4ZHxqqfBKM6iNQZW2OMTyxNbA2WC7SHWHtGVpFzWtQAuS2pspkWVzdEBXXx8Ik0Za4w== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-function-sent@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.2.0.tgz#91474d4d400604e4c6cbd4d77cd6cb3b8565576c" - integrity sha512-2MOVuJ6IMAifp2cf0RFkHQaOvHpbBYyWCvgtF/WVqXhTd7Bgtov8iXVCadLXp2FN1BrI2EFl+JXuwXy0qr3KoQ== +"@babel/plugin-syntax-export-namespace-from@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-json-strings@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470" - integrity sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg== +"@babel/plugin-syntax-function-sent@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.8.3.tgz#5a4874bdfc271f0fa1c470bf508dc54af3041e19" + integrity sha512-NNEutF0x2PdWYij2bmf/i50dSq4SUdgFij4BZwj3I4qDZgql3dlFJRyvwGHAhwKYElUKHaP0wQ/yO1d/enpJaw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-jsx@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz#0b85a3b4bc7cdf4cc4b8bf236335b907ca22e7c7" - integrity sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw== +"@babel/plugin-syntax-json-strings@^7.7.4", "@babel/plugin-syntax-json-strings@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-nullish-coalescing-operator@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.2.0.tgz#f75083dfd5ade73e783db729bbd87e7b9efb7624" - integrity sha512-lRCEaKE+LTxDQtgbYajI04ddt6WW0WJq57xqkAZ+s11h4YgfRHhVA/Y2VhfPzzFD4qeLHWg32DMp9HooY4Kqlg== +"@babel/plugin-syntax-jsx@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94" + integrity sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-numeric-separator@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.2.0.tgz#7470fe070c2944469a756752a69a6963135018be" - integrity sha512-DroeVNkO/BnGpL2R7+ZNZqW+E24aR/4YWxP3Qb15d6lPU8KDzF8HlIUIRCOJRn4X77/oyW4mJY+7FHfY82NLtQ== +"@babel/plugin-syntax-nullish-coalescing-operator@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-object-rest-spread@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" - integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== +"@babel/plugin-syntax-numeric-separator@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" + integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-optional-catch-binding@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c" - integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w== +"@babel/plugin-syntax-object-rest-spread@^7.7.4", "@babel/plugin-syntax-object-rest-spread@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-chaining@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.2.0.tgz#a59d6ae8c167e7608eaa443fda9fa8fa6bf21dff" - integrity sha512-HtGCtvp5Uq/jH/WNUPkK6b7rufnCPLLlDAFN7cmACoIjaOOiXxUt3SswU5loHqrhtqTsa/WoLQ1OQ1AGuZqaWA== +"@babel/plugin-syntax-optional-catch-binding@^7.7.4", "@babel/plugin-syntax-optional-catch-binding@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-throw-expressions@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.2.0.tgz#79001ee2afe1b174b1733cdc2fc69c9a46a0f1f8" - integrity sha512-ngwynuqu1Rx0JUS9zxSDuPgW1K8TyVZCi2hHehrL4vyjqE7RGoNHWlZsS7KQT2vw9Yjk4YLa0+KldBXTRdPLRg== +"@babel/plugin-syntax-optional-chaining@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-transform-arrow-functions@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" - integrity sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg== +"@babel/plugin-syntax-throw-expressions@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.8.3.tgz#c763bcf26d202ddb65f1299a29d63aad312adb54" + integrity sha512-Mv3shY1i7ZssY4OY+eLZJAmNCwqTcpv2qOKO9x6irELSygfKWVSMXk0igJsA9UhU4hOdw0qMGkjj9TAk4MqzwQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-async-to-generator@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz#89a3848a0166623b5bc481164b5936ab947e887e" - integrity sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg== +"@babel/plugin-syntax-top-level-await@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391" + integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g== dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-remap-async-to-generator" "^7.1.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-block-scoped-functions@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190" - integrity sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w== +"@babel/plugin-transform-arrow-functions@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6" + integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-block-scoping@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz#a35f395e5402822f10d2119f6f8e045e3639a2ce" - integrity sha512-82A3CLRRdYubkG85lKwhZB0WZoHxLGsJdux/cOVaJCJpvYFl1LVzAIFyRsa7CvXqW8rBM4Zf3Bfn8PHt5DP0Sg== +"@babel/plugin-transform-async-to-generator@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086" + integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - lodash "^4.17.13" + "@babel/helper-module-imports" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-remap-async-to-generator" "^7.8.3" -"@babel/plugin-transform-classes@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.5.5.tgz#d094299d9bd680a14a2a0edae38305ad60fb4de9" - integrity sha512-U2htCNK/6e9K7jGyJ++1p5XRU+LJjrwtoiVn9SzRlDT2KubcZ11OOwy3s24TjHxPgxNwonCYP7U2K51uVYCMDg== +"@babel/plugin-transform-block-scoped-functions@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3" + integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg== dependencies: - "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-define-map" "^7.5.5" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.5.5" - "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-transform-block-scoping@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a" + integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + lodash "^4.17.13" + +"@babel/plugin-transform-classes@^7.7.4": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.6.tgz#77534447a477cbe5995ae4aee3e39fbc8090c46d" + integrity sha512-k9r8qRay/R6v5aWZkrEclEhKO6mc1CCQr2dLsVHBmOQiMpN6I2bpjX3vgnldUWeEI1GHVNByULVxZ4BdP4Hmdg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/helper-define-map" "^7.8.3" + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-optimise-call-expression" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.6" + "@babel/helper-split-export-declaration" "^7.8.3" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da" - integrity sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA== +"@babel/plugin-transform-computed-properties@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b" + integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-destructuring@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a" - integrity sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ== +"@babel/plugin-transform-destructuring@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.3.tgz#20ddfbd9e4676906b1056ee60af88590cc7aaa0b" + integrity sha512-H4X646nCkiEcHZUZaRkhE2XVsoz0J/1x3VVujnn96pSoGCtKPA99ZZA+va+gK+92Zycd6OBKCD8tDb/731bhgQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3" - integrity sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg== +"@babel/plugin-transform-dotall-regex@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" + integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.4.4" - regexpu-core "^4.5.4" + "@babel/helper-create-regexp-features-plugin" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-duplicate-keys@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz#c5dbf5106bf84cdf691222c0974c12b1df931853" - integrity sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ== +"@babel/plugin-transform-duplicate-keys@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1" + integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-exponentiation-operator@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008" - integrity sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A== +"@babel/plugin-transform-exponentiation-operator@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7" + integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-for-of@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556" - integrity sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ== +"@babel/plugin-transform-for-of@^7.7.4": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.6.tgz#a051bd1b402c61af97a27ff51b468321c7c2a085" + integrity sha512-M0pw4/1/KI5WAxPsdcUL/w2LJ7o89YHN3yLkzNjg7Yl15GlVGgzHyCU+FMeAxevHGsLVmUqbirlUIKTafPmzdw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-function-name@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz#e1436116abb0610c2259094848754ac5230922ad" - integrity sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA== +"@babel/plugin-transform-function-name@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b" + integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ== dependencies: - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-literals@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1" - integrity sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg== +"@babel/plugin-transform-literals@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1" + integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-member-expression-literals@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz#fa10aa5c58a2cb6afcf2c9ffa8cb4d8b3d489a2d" - integrity sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA== +"@babel/plugin-transform-member-expression-literals@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410" + integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-modules-amd@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz#ef00435d46da0a5961aa728a1d2ecff063e4fb91" - integrity sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg== +"@babel/plugin-transform-modules-amd@^7.7.5": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz#65606d44616b50225e76f5578f33c568a0b876a5" + integrity sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ== dependencies: - "@babel/helper-module-transforms" "^7.1.0" - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-module-transforms" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-commonjs@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74" - integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ== +"@babel/plugin-transform-modules-commonjs@^7.7.5": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.8.3.tgz#df251706ec331bd058a34bdd72613915f82928a5" + integrity sha512-JpdMEfA15HZ/1gNuB9XEDlZM1h/gF/YOH7zaZzQu2xCFRfwc01NXBMHHSTT6hRjlXJJs5x/bfODM3LiCk94Sxg== dependencies: - "@babel/helper-module-transforms" "^7.4.4" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-simple-access" "^7.1.0" + "@babel/helper-module-transforms" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-simple-access" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-systemjs@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz#e75266a13ef94202db2a0620977756f51d52d249" - integrity sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg== +"@babel/plugin-transform-modules-systemjs@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.8.3.tgz#d8bbf222c1dbe3661f440f2f00c16e9bb7d0d420" + integrity sha512-8cESMCJjmArMYqa9AO5YuMEkE4ds28tMpZcGZB/jl3n0ZzlsxOAi3mC+SKypTfT8gjMupCnd3YiXCkMjj2jfOg== dependencies: - "@babel/helper-hoist-variables" "^7.4.4" - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-hoist-variables" "^7.8.3" + "@babel/helper-module-transforms" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-umd@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae" - integrity sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw== +"@babel/plugin-transform-modules-umd@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.8.3.tgz#592d578ce06c52f5b98b02f913d653ffe972661a" + integrity sha512-evhTyWhbwbI3/U6dZAnx/ePoV7H6OUG+OjiJFHmhr9FPn0VShjwC2kdxqIuQ/+1P50TMrneGzMeyMTFOjKSnAw== dependencies: - "@babel/helper-module-transforms" "^7.1.0" - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-module-transforms" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-named-capturing-groups-regex@^7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz#9d269fd28a370258199b4294736813a60bbdd106" - integrity sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg== +"@babel/plugin-transform-named-capturing-groups-regex@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c" + integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw== dependencies: - regexp-tree "^0.1.6" + "@babel/helper-create-regexp-features-plugin" "^7.8.3" -"@babel/plugin-transform-new-target@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz#18d120438b0cc9ee95a47f2c72bc9768fbed60a5" - integrity sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA== +"@babel/plugin-transform-new-target@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43" + integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-object-super@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz#c70021df834073c65eb613b8679cc4a381d1a9f9" - integrity sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ== +"@babel/plugin-transform-object-super@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725" + integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.5.5" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.3" -"@babel/plugin-transform-parameters@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz#7556cf03f318bd2719fe4c922d2d808be5571e16" - integrity sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw== +"@babel/plugin-transform-parameters@^7.7.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.4.tgz#1d5155de0b65db0ccf9971165745d3bb990d77d3" + integrity sha512-IsS3oTxeTsZlE5KqzTbcC2sV0P9pXdec53SU+Yxv7o/6dvGM5AkTotQKhoSffhNgZ/dftsSiOoxy7evCYJXzVA== dependencies: - "@babel/helper-call-delegate" "^7.4.4" - "@babel/helper-get-function-arity" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-call-delegate" "^7.8.3" + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-property-literals@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz#03e33f653f5b25c4eb572c98b9485055b389e905" - integrity sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ== +"@babel/plugin-transform-property-literals@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263" + integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-react-display-name@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz#ebfaed87834ce8dc4279609a4f0c324c156e3eb0" - integrity sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A== +"@babel/plugin-transform-react-display-name@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.8.3.tgz#70ded987c91609f78353dd76d2fb2a0bb991e8e5" + integrity sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-react-jsx-self@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz#461e21ad9478f1031dd5e276108d027f1b5240ba" - integrity sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg== +"@babel/plugin-transform-react-jsx-self@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.8.3.tgz#c4f178b2aa588ecfa8d077ea80d4194ee77ed702" + integrity sha512-01OT7s5oa0XTLf2I8XGsL8+KqV9lx3EZV+jxn/L2LQ97CGKila2YMroTkCEIE0HV/FF7CMSRsIAybopdN9NTdg== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-jsx" "^7.2.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-jsx" "^7.8.3" -"@babel/plugin-transform-react-jsx-source@^7.0.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz#583b10c49cf057e237085bcbd8cc960bd83bd96b" - integrity sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg== +"@babel/plugin-transform-react-jsx-source@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.8.3.tgz#951e75a8af47f9f120db731be095d2b2c34920e0" + integrity sha512-PLMgdMGuVDtRS/SzjNEQYUT8f4z1xb2BAT54vM1X5efkVuYBf5WyGUMbpmARcfq3NaglIwz08UVQK4HHHbC6ag== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-jsx" "^7.2.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-jsx" "^7.8.3" -"@babel/plugin-transform-react-jsx@^7.0.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz#f2cab99026631c767e2745a5368b331cfe8f5290" - integrity sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg== +"@babel/plugin-transform-react-jsx@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.8.3.tgz#4220349c0390fdefa505365f68c103562ab2fc4a" + integrity sha512-r0h+mUiyL595ikykci+fbwm9YzmuOrUBi0b+FDIKmi3fPQyFokWVEMJnRWHJPPQEjyFJyna9WZC6Viv6UHSv1g== dependencies: - "@babel/helper-builder-react-jsx" "^7.3.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-jsx" "^7.2.0" + "@babel/helper-builder-react-jsx" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-jsx" "^7.8.3" -"@babel/plugin-transform-regenerator@^7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz#629dc82512c55cee01341fb27bdfcb210354680f" - integrity sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA== +"@babel/plugin-transform-regenerator@^7.7.5": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.3.tgz#b31031e8059c07495bf23614c97f3d9698bc6ec8" + integrity sha512-qt/kcur/FxrQrzFR432FGZznkVAjiyFtCOANjkAKwCbt465L6ZCiUQh2oMYGU3Wo8LRFJxNDFwWn106S5wVUNA== dependencies: regenerator-transform "^0.14.0" -"@babel/plugin-transform-reserved-words@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz#4792af87c998a49367597d07fedf02636d2e1634" - integrity sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw== +"@babel/plugin-transform-reserved-words@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5" + integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-shorthand-properties@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0" - integrity sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg== +"@babel/plugin-transform-shorthand-properties@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8" + integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-spread@^7.2.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406" - integrity sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w== +"@babel/plugin-transform-spread@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8" + integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-sticky-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1" - integrity sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw== +"@babel/plugin-transform-sticky-regex@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100" + integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-regex" "^7.8.3" -"@babel/plugin-transform-template-literals@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz#9d28fea7bbce637fb7612a0750989d8321d4bcb0" - integrity sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g== +"@babel/plugin-transform-template-literals@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80" + integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-typeof-symbol@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2" - integrity sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw== +"@babel/plugin-transform-typeof-symbol@^7.7.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412" + integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-unicode-regex@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f" - integrity sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA== +"@babel/plugin-transform-unicode-regex@^7.7.4": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad" + integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.4.4" - regexpu-core "^4.5.4" + "@babel/helper-create-regexp-features-plugin" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" -"@babel/preset-env@7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.5.tgz#bc470b53acaa48df4b8db24a570d6da1fef53c9a" - integrity sha512-GMZQka/+INwsMz1A5UEql8tG015h5j/qjptpKY2gJ7giy8ohzU710YciJB5rcKsWGWHiW3RUnHib0E5/m3Tp3A== +"@babel/preset-env@7.7.5": + version "7.7.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.7.5.tgz#f28573ed493edb4ba763b37fb4fbb85601469370" + integrity sha512-wDPbiaZdGzsJuTWlpLHJxmwslwHGLZ8F5v69zX3oAWeTOFWdy4OJHoTKg26oAnFg052v+/LAPY5os9KB0LrOEA== dependencies: - "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-module-imports" "^7.7.4" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-async-generator-functions" "^7.2.0" - "@babel/plugin-proposal-dynamic-import" "^7.5.0" - "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.5.5" - "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-syntax-async-generators" "^7.2.0" - "@babel/plugin-syntax-dynamic-import" "^7.2.0" - "@babel/plugin-syntax-json-strings" "^7.2.0" - "@babel/plugin-syntax-object-rest-spread" "^7.2.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" - "@babel/plugin-transform-arrow-functions" "^7.2.0" - "@babel/plugin-transform-async-to-generator" "^7.5.0" - "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.5.5" - "@babel/plugin-transform-classes" "^7.5.5" - "@babel/plugin-transform-computed-properties" "^7.2.0" - "@babel/plugin-transform-destructuring" "^7.5.0" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/plugin-transform-duplicate-keys" "^7.5.0" - "@babel/plugin-transform-exponentiation-operator" "^7.2.0" - "@babel/plugin-transform-for-of" "^7.4.4" - "@babel/plugin-transform-function-name" "^7.4.4" - "@babel/plugin-transform-literals" "^7.2.0" - "@babel/plugin-transform-member-expression-literals" "^7.2.0" - "@babel/plugin-transform-modules-amd" "^7.5.0" - "@babel/plugin-transform-modules-commonjs" "^7.5.0" - "@babel/plugin-transform-modules-systemjs" "^7.5.0" - "@babel/plugin-transform-modules-umd" "^7.2.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5" - "@babel/plugin-transform-new-target" "^7.4.4" - "@babel/plugin-transform-object-super" "^7.5.5" - "@babel/plugin-transform-parameters" "^7.4.4" - "@babel/plugin-transform-property-literals" "^7.2.0" - "@babel/plugin-transform-regenerator" "^7.4.5" - "@babel/plugin-transform-reserved-words" "^7.2.0" - "@babel/plugin-transform-shorthand-properties" "^7.2.0" - "@babel/plugin-transform-spread" "^7.2.0" - "@babel/plugin-transform-sticky-regex" "^7.2.0" - "@babel/plugin-transform-template-literals" "^7.4.4" - "@babel/plugin-transform-typeof-symbol" "^7.2.0" - "@babel/plugin-transform-unicode-regex" "^7.4.4" - "@babel/types" "^7.5.5" + "@babel/plugin-proposal-async-generator-functions" "^7.7.4" + "@babel/plugin-proposal-dynamic-import" "^7.7.4" + "@babel/plugin-proposal-json-strings" "^7.7.4" + "@babel/plugin-proposal-object-rest-spread" "^7.7.4" + "@babel/plugin-proposal-optional-catch-binding" "^7.7.4" + "@babel/plugin-proposal-unicode-property-regex" "^7.7.4" + "@babel/plugin-syntax-async-generators" "^7.7.4" + "@babel/plugin-syntax-dynamic-import" "^7.7.4" + "@babel/plugin-syntax-json-strings" "^7.7.4" + "@babel/plugin-syntax-object-rest-spread" "^7.7.4" + "@babel/plugin-syntax-optional-catch-binding" "^7.7.4" + "@babel/plugin-syntax-top-level-await" "^7.7.4" + "@babel/plugin-transform-arrow-functions" "^7.7.4" + "@babel/plugin-transform-async-to-generator" "^7.7.4" + "@babel/plugin-transform-block-scoped-functions" "^7.7.4" + "@babel/plugin-transform-block-scoping" "^7.7.4" + "@babel/plugin-transform-classes" "^7.7.4" + "@babel/plugin-transform-computed-properties" "^7.7.4" + "@babel/plugin-transform-destructuring" "^7.7.4" + "@babel/plugin-transform-dotall-regex" "^7.7.4" + "@babel/plugin-transform-duplicate-keys" "^7.7.4" + "@babel/plugin-transform-exponentiation-operator" "^7.7.4" + "@babel/plugin-transform-for-of" "^7.7.4" + "@babel/plugin-transform-function-name" "^7.7.4" + "@babel/plugin-transform-literals" "^7.7.4" + "@babel/plugin-transform-member-expression-literals" "^7.7.4" + "@babel/plugin-transform-modules-amd" "^7.7.5" + "@babel/plugin-transform-modules-commonjs" "^7.7.5" + "@babel/plugin-transform-modules-systemjs" "^7.7.4" + "@babel/plugin-transform-modules-umd" "^7.7.4" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.7.4" + "@babel/plugin-transform-new-target" "^7.7.4" + "@babel/plugin-transform-object-super" "^7.7.4" + "@babel/plugin-transform-parameters" "^7.7.4" + "@babel/plugin-transform-property-literals" "^7.7.4" + "@babel/plugin-transform-regenerator" "^7.7.5" + "@babel/plugin-transform-reserved-words" "^7.7.4" + "@babel/plugin-transform-shorthand-properties" "^7.7.4" + "@babel/plugin-transform-spread" "^7.7.4" + "@babel/plugin-transform-sticky-regex" "^7.7.4" + "@babel/plugin-transform-template-literals" "^7.7.4" + "@babel/plugin-transform-typeof-symbol" "^7.7.4" + "@babel/plugin-transform-unicode-regex" "^7.7.4" + "@babel/types" "^7.7.4" browserslist "^4.6.0" - core-js-compat "^3.1.1" + core-js-compat "^3.4.7" invariant "^2.2.2" js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/preset-react@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0" - integrity sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w== +"@babel/preset-react@7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.7.4.tgz#3fe2ea698d8fb536d8e7881a592c3c1ee8bf5707" + integrity sha512-j+vZtg0/8pQr1H8wKoaJyGL2IEk3rG/GIvua7Sec7meXVIvGycihlGMx5xcU00kqCJbwzHs18xTu3YfREOqQ+g== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-transform-react-display-name" "^7.0.0" - "@babel/plugin-transform-react-jsx" "^7.0.0" - "@babel/plugin-transform-react-jsx-self" "^7.0.0" - "@babel/plugin-transform-react-jsx-source" "^7.0.0" + "@babel/plugin-transform-react-display-name" "^7.7.4" + "@babel/plugin-transform-react-jsx" "^7.7.4" + "@babel/plugin-transform-react-jsx-self" "^7.7.4" + "@babel/plugin-transform-react-jsx-source" "^7.7.4" "@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0", "@babel/runtime@^7.5.5": version "7.5.5" @@ -812,6 +923,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.6.3": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" + integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/template@^7.1.0", "@babel/template@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" @@ -821,7 +939,16 @@ "@babel/parser" "^7.4.4" "@babel/types" "^7.4.4" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5": +"@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" + integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" + +"@babel/traverse@^7.0.0", "@babel/traverse@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ== @@ -836,7 +963,22 @@ globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": +"@babel/traverse@^7.7.4", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.4", "@babel/traverse@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.6.tgz#acfe0c64e1cd991b3e32eae813a6eb564954b5ff" + integrity sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.6" + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/types@^7.0.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" integrity sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw== @@ -845,43 +987,59 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@fortawesome/fontawesome-common-types@^0.2.22": - version "0.2.22" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.22.tgz#3f1328d232a0fd5de8484d833c8519426f39f016" - integrity sha512-QmEuZsipX5/cR9JOg0fsTN4Yr/9lieYWM8AQpmRa0eIfeOcl/HLYoEa366BCGRSrgNJEexuvOgbq9jnJ22IY5g== +"@babel/types@^7.7.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.6.tgz#629ecc33c2557fcde7126e58053127afdb3e6d01" + integrity sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + +"@cnakazawa/watch@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" + integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@fortawesome/fontawesome-common-types@^0.2.25": + version "0.2.27" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.27.tgz#19706345859fc46adf3684ed01d11b40903b87e9" + integrity sha512-97GaByGaXDGMkzcJX7VmR/jRJd8h1mfhtA7RsxDBN61GnWE/PPCZhOdwG/8OZYktiRUF0CvFOr+VgRkJrt6TWg== -"@fortawesome/fontawesome-free@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.10.2.tgz#27e02da1e34b50c9869179d364fb46627b521130" - integrity sha512-9pw+Nsnunl9unstGEHQ+u41wBEQue6XPBsILXtJF/4fNN1L3avJcMF/gGF86rIjeTAgfLjTY9ndm68/X4f4idQ== +"@fortawesome/fontawesome-free@5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.2.tgz#8644bc25b19475779a7b7c1fc104bc0a794f4465" + integrity sha512-XiUPoS79r1G7PcpnNtq85TJ7inJWe0v+b5oZJZKb0pGHNIV6+UiNeQWiFGmuQ0aj7GEhnD/v9iqxIsjuRKtEnQ== -"@fortawesome/fontawesome-svg-core@1.2.22": - version "1.2.22" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.22.tgz#9a6117c96c8b823c7d531000568ac75c3c02e123" - integrity sha512-Q941E4x8UfnMH3308n0qrgoja+GoqyiV846JTLoCcCWAKokLKrixCkq6RDBs8r+TtAWaLUrBpI+JFxQNX/WNPQ== +"@fortawesome/fontawesome-svg-core@1.2.25": + version "1.2.25" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.25.tgz#24b03391d14f0c6171e8cad7057c687b74049790" + integrity sha512-MotKnn53JKqbkLQiwcZSBJVYtTgIKFbh7B8+kd05TSnfKYPFmjKKI59o2fpz5t0Hzl35vVGU6+N4twoOpZUrqA== dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.22" + "@fortawesome/fontawesome-common-types" "^0.2.25" -"@fortawesome/free-regular-svg-icons@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.10.2.tgz#e4ada1c15f42133ad92761418a9b0e2d407fb022" - integrity sha512-Qk4FmwXuRDY5K2GyiKt7adCN204dTlTb0Ps3/JU4BfYoCrU43DResd1QZxfcoQJfV2kw29spZ4+BDL+9IRyj1Q== +"@fortawesome/free-regular-svg-icons@5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.11.2.tgz#6edfc5c230094be3b9070fef048c01aa321a8428" + integrity sha512-k0vbThRv9AvnXYBWi1gn1rFW4X7co/aFkbm0ZNmAR5PoWb9vY9EDDDobg8Ay4ISaXtCPypvJ0W1FWkSpLQwZ6w== dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.22" + "@fortawesome/fontawesome-common-types" "^0.2.25" -"@fortawesome/free-solid-svg-icons@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.10.2.tgz#61bcecce3aa5001fd154826238dfa840de4aa05a" - integrity sha512-9Os/GRUcy+iVaznlg8GKcPSQFpIQpAg14jF0DWsMdnpJfIftlvfaQCWniR/ex9FoOpSEOrlXqmUCFL+JGeciuA== +"@fortawesome/free-solid-svg-icons@5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.11.2.tgz#2f2f1459743a27902b76655a0d0bc5ec4d945631" + integrity sha512-zBue4i0PAZJUXOmLBBvM7L0O7wmsDC8dFv9IhpW5QL4kT9xhhVUsYg/LX1+5KaukWq4/cbDcKT+RT1aRe543sg== dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.22" + "@fortawesome/fontawesome-common-types" "^0.2.25" -"@fortawesome/react-fontawesome@0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.4.tgz#18d61d9b583ca289a61aa7dccc05bd164d6bc9ad" - integrity sha512-GwmxQ+TK7PEdfSwvxtGnMCqrfEm0/HbRHArbUudsYiy9KzVCwndxa2KMcfyTQ8El0vROrq8gOOff09RF1oQe8g== +"@fortawesome/react-fontawesome@0.1.8": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.8.tgz#cb6d4dd3aeec45b6ff2d48c812317a6627618511" + integrity sha512-I5h9YQg/ePA3Br9ISS18fcwOYmzQYDSM1ftH03/8nHkiqIVHtUyQBw482+60dnzvlr82gHt3mGm+nDUp159FCw== dependencies: - humps "^2.0.1" prop-types "^15.5.10" "@gulp-sourcemaps/identity-map@1.X": @@ -928,11 +1086,24 @@ "@nodelib/fs.stat" "2.0.2" run-parallel "^1.1.9" +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + "@nodelib/fs.stat@2.0.2", "@nodelib/fs.stat@^2.0.1": version "2.0.2" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.2.tgz#2762aea8fe78ea256860182dcb52d61ee4b8fda6" integrity sha512-z8+wGWV2dgUhLqrtRYa03yDx4HWMvXKi1z8g3m2JyxAx8F7xk74asqPk5LAETjqDSGLFML/6CDl0+yFunSYicw== +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + "@nodelib/fs.stat@^1.1.2": version "1.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" @@ -946,14 +1117,22 @@ "@nodelib/fs.scandir" "2.1.2" fastq "^1.6.0" -"@sentry/browser@5.6.3": - version "5.6.3" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.6.3.tgz#5cc37b0443eba55ad13c13d34d6b95ff30dfbfe3" - integrity sha512-bP1LTbcKPOkkmfJOAM6c7WZ0Ov0ZEW6B9keVZ9wH9fw/lBPd9UyDMDCwJ+FAYKz9M9S5pxQeJ4Ebd7WUUrGVAQ== +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== dependencies: - "@sentry/core" "5.6.2" - "@sentry/types" "5.6.1" - "@sentry/utils" "5.6.1" + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@sentry/browser@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.11.0.tgz#70b6e42e53d9ed8f0157101f63a8f72ac98cbd6f" + integrity sha512-+POFe768M6de+y6IK1jB+zXXpSPSekQ47retE5YLuGwdI5vBgB7V7/Zcv++Vrr5TR+TOwBxNQEuq7Z/bySeksw== + dependencies: + "@sentry/core" "5.11.0" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.0" tslib "^1.9.3" "@sentry/cli@1.47.1": @@ -968,55 +1147,55 @@ progress "2.0.0" proxy-from-env "^1.0.0" -"@sentry/core@5.6.2": - version "5.6.2" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.6.2.tgz#8c5477654a83ebe41a72e86a79215deb5025e418" - integrity sha512-grbjvNmyxP5WSPR6UobN2q+Nss7Hvz+BClBT8QTr7VTEG5q89TwNddn6Ej3bGkaUVbct/GpVlI3XflWYDsnU6Q== +"@sentry/core@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.11.0.tgz#fc21acefbb58bacda7b1b33e4a67b56d7f7064b4" + integrity sha512-bofpzY5Sgcrq69eg1iA13kGJqWia4s/jVOB3DCU3rPUKGHVL8hh9CjrIho1C0XygQxjuPAJznOj0cCaRxD1vJQ== dependencies: - "@sentry/hub" "5.6.1" - "@sentry/minimal" "5.6.1" - "@sentry/types" "5.6.1" - "@sentry/utils" "5.6.1" + "@sentry/hub" "5.11.0" + "@sentry/minimal" "5.11.0" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.0" tslib "^1.9.3" -"@sentry/hub@5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.6.1.tgz#9f355c0abcc92327fbd10b9b939608aa4967bece" - integrity sha512-m+OhkIV5yTAL3R1+XfCwzUQka0UF/xG4py8sEfPXyYIcoOJ2ZTX+1kQJLy8QQJ4RzOBwZA+DzRKP0cgzPJ3+oQ== +"@sentry/hub@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.11.0.tgz#d427432fff13d9b34d83da8651ad5c4207260796" + integrity sha512-ZtCcbq3BLkQo/y07amvP21ZjmL7up/fD1032XrA+44U7M1d2w+CDCVRWcCJGK/otzPz7cw8yc5oS4Cn68wLVxw== dependencies: - "@sentry/types" "5.6.1" - "@sentry/utils" "5.6.1" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.0" tslib "^1.9.3" -"@sentry/integrations@5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-5.6.1.tgz#fcee1a6e5535a07fdefd365178662283279ce0d7" - integrity sha512-bPtJbmhLDH9Exy0luIKxjlfqmuyAjUPTHZ2CLIw6YlhA5WgK9aYyyjLHTmWK+E9baZBqSp0ShVPAgue2jfpQmQ== +"@sentry/integrations@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-5.11.0.tgz#8f14fbed44464db7b5fa5e1b86b0dec8fe543ff9" + integrity sha512-GUQ0/AnRPl3jxF0kaQtaHVnDzhmd6SfI1/Ob5sVZeBpQ5cVJ4bNICirxNpW8X6J9M8YNzaRCv8w1J/o9gWTF5g== dependencies: - "@sentry/types" "5.6.1" - "@sentry/utils" "5.6.1" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.0" tslib "^1.9.3" -"@sentry/minimal@5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.6.1.tgz#09d92b26de0b24555cd50c3c33ba4c3e566009a1" - integrity sha512-ercCKuBWHog6aS6SsJRuKhJwNdJ2oRQVWT2UAx1zqvsbHT9mSa8ZRjdPHYOtqY3DoXKk/pLUFW/fkmAnpdMqRw== +"@sentry/minimal@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.11.0.tgz#5a5f334794f03044e7d0316757abd0a236fcb1f3" + integrity sha512-fplz8sCmYE9Hdm+qnoATls5FPKjVyXcCuav9UKFLV6L+MAPjWVINbHFPBcYAmR5bjK4/Otfi1SPCBe1MQT/FtA== dependencies: - "@sentry/hub" "5.6.1" - "@sentry/types" "5.6.1" + "@sentry/hub" "5.11.0" + "@sentry/types" "5.11.0" tslib "^1.9.3" -"@sentry/types@5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.6.1.tgz#5915e1ee4b7a678da3ac260c356b1cb91139a299" - integrity sha512-Kub8TETefHpdhvtnDj3kKfhCj0u/xn3Zi2zIC7PB11NJHvvPXENx97tciz4roJGp7cLRCJsFqCg4tHXniqDSnQ== +"@sentry/types@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.11.0.tgz#40f0f3174362928e033ddd9725d55e7c5cb7c5b6" + integrity sha512-1Uhycpmeo1ZK2GLvrtwZhTwIodJHcyIS6bn+t4IMkN9MFoo6ktbAfhvexBDW/IDtdLlCGJbfm8nIZerxy0QUpg== -"@sentry/utils@5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.6.1.tgz#69d9e151e50415bc91f2428e3bcca8beb9bc2815" - integrity sha512-rfgha+UsHW816GqlSRPlniKqAZylOmQWML2JsujoUP03nPu80zdN43DK9Poy/d9OxBxv0gd5K2n+bFdM2kqLQQ== +"@sentry/utils@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.11.0.tgz#c07313eaf2331ecdecfd240c350bb28c7bd38e9c" + integrity sha512-84MNM08ANmda/tWMBCCb9tga0b4ZD7tSo0i20RJalkdLk9zJmmepKw+sA5PyztO/YxkqAt9KijSmtIafd0LlOQ== dependencies: - "@sentry/types" "5.6.1" + "@sentry/types" "5.11.0" tslib "^1.9.3" "@types/asap@^2.0.0": @@ -1024,6 +1203,11 @@ resolved "https://registry.yarnpkg.com/@types/asap/-/asap-2.0.0.tgz#d529e9608c83499a62ae08c871c5e62271aa2963" integrity sha512-upIS0Gt9Mc8eEpCbYMZ1K8rhNosfKUtimNcINce+zLwJF5UpM3Vv7yz3S5l/1IX+DxTa8lTkUjqynvjRXyJzsg== +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -1056,11 +1240,26 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/minimist@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" + integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= + "@types/node@*": version "12.7.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44" integrity sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg== +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + "@types/prop-types@*": version "15.7.1" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" @@ -1267,10 +1466,10 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -acorn-jsx@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f" - integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw== +acorn-jsx@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" + integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== acorn@5.X, acorn@^5.0.3: version "5.7.3" @@ -1282,10 +1481,10 @@ acorn@^6.2.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== -acorn@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.0.0.tgz#26b8d1cd9a9b700350b71c0905546f64d1284e7a" - integrity sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ== +acorn@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" + integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== add-px-to-style@1.0.0: version "1.0.0" @@ -1356,10 +1555,12 @@ ansi-cyan@^0.1.1: dependencies: ansi-wrap "0.1.0" -ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.2.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + dependencies: + type-fest "^0.11.0" ansi-gray@^0.1.1: version "0.1.1" @@ -1390,6 +1591,11 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -1402,6 +1608,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + ansi-wrap@0.1.0, ansi-wrap@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" @@ -1509,11 +1723,6 @@ array-each@^1.0.0, array-each@^1.0.1: resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-includes@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" @@ -1522,6 +1731,15 @@ array-includes@^3.0.3: define-properties "^1.1.2" es-abstract "^1.7.0" +array-includes@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" + integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0" + is-string "^1.0.5" + array-initial@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/array-initial/-/array-initial-1.1.0.tgz#2fa74b26739371c3947bd7a7adc73be334b3d795" @@ -1556,7 +1774,7 @@ array-sort@^1.0.0: get-value "^2.0.6" kind-of "^5.0.2" -array-union@^1.0.1, array-union@^1.0.2: +array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= @@ -1669,18 +1887,31 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@9.6.1, autoprefixer@^9.5.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47" - integrity sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw== +autoprefixer@9.7.3: + version "9.7.3" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.3.tgz#fd42ed03f53de9beb4ca0d61fb4f7268a9bb50b4" + integrity sha512-8T5Y1C5Iyj6PgkPSFd0ODvK9DIleuPKUPYniNxybS47g2k2wFgLZ46lGQHlBuGKIAEV8fbCDfKCCRS1tvOgc3Q== dependencies: - browserslist "^4.6.3" - caniuse-lite "^1.0.30000980" + browserslist "^4.8.0" + caniuse-lite "^1.0.30001012" chalk "^2.4.2" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.17" - postcss-value-parser "^4.0.0" + postcss "^7.0.23" + postcss-value-parser "^4.0.2" + +autoprefixer@^9.7.3: + version "9.7.4" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378" + integrity sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g== + dependencies: + browserslist "^4.8.3" + caniuse-lite "^1.0.30001020" + chalk "^2.4.2" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.26" + postcss-value-parser "^4.0.2" aws-sign2@~0.7.0: version "0.7.0" @@ -1809,7 +2040,7 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== -bindings@^1.2.1: +bindings@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== @@ -1945,7 +2176,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.0.0, browserslist@^4.6.0, browserslist@^4.6.3, browserslist@^4.6.6: +browserslist@^4.0.0, browserslist@^4.6.0: version "4.6.6" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453" integrity sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA== @@ -1954,6 +2185,15 @@ browserslist@^4.0.0, browserslist@^4.6.0, browserslist@^4.6.3, browserslist@^4.6 electron-to-chromium "^1.3.191" node-releases "^1.1.25" +browserslist@^4.8.0, browserslist@^4.8.3: + version "4.9.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.9.1.tgz#01ffb9ca31a1aef7678128fc6a2253316aa7287c" + integrity sha512-Q0DnKq20End3raFulq6Vfp1ecB9fh8yUNV55s8sekaDDeqBaCtWlRHCUdaWyUeSSBJM7IbM6HcsyaeYqgeDhnw== + dependencies: + caniuse-lite "^1.0.30001030" + electron-to-chromium "^1.3.363" + node-releases "^1.1.50" + bser@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.0.tgz#65fc784bf7f87c009b973c12db6546902fa9c7b5" @@ -2080,26 +2320,21 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -camelcase-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" - integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= +camelcase-keys@^6.1.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.1.2.tgz#531a289aeea93249b63ec1249db9265f305041f7" + integrity sha512-QfFrU0CIw2oltVvpndW32kuJ/9YOJwUnmWrjlXt1nnJZHCaS9i6bfOpg9R4Lw8aZjStkJWM+jc0cdXjWBgVJSw== dependencies: - camelcase "^4.1.0" - map-obj "^2.0.0" - quick-lru "^1.0.0" + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - -camelcase@^5.3.1: +camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== @@ -2114,11 +2349,23 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000984: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000984: version "1.0.30000989" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz#b9193e293ccf7e4426c5245134b8f2a56c0ac4b9" integrity sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw== +caniuse-lite@^1.0.30001012, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001030: + version "1.0.30001031" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001031.tgz#76f1bdd39e19567b855302f65102d9a8aaad5930" + integrity sha512-DpAP5a1NGRLgYfaNCaXIRyGARi+3tJA2quZXNNA1Du26VyVkqvy2tznNu5ANyN1Y5aX44QDotZSVSUSi2uMGjg== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -2149,6 +2396,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + character-entities-html4@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.3.tgz#5ce6e01618e47048ac22f34f7f39db5c6fd679ef" @@ -2223,7 +2478,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@2.2.6: +classnames@2.2.6, classnames@^2.2.0: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -2240,12 +2495,12 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: - restore-cursor "^2.0.0" + restore-cursor "^3.1.0" cli-width@^2.0.0: version "2.2.0" @@ -2270,6 +2525,15 @@ cliui@^3.2.0: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -2359,12 +2623,19 @@ color-convert@^1.3.0, color-convert@^1.9.0, color-convert@^1.9.1: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -2460,10 +2731,10 @@ concat-with-sourcemaps@^1.0.0: dependencies: source-map "^0.6.1" -connected-react-router@6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.5.2.tgz#422af70f86cb276681e20ab4295cf27dd9b6c7e3" - integrity sha512-qzsLPZCofSI80fwy+HgxtEgSGS4ndYUUZAWaw1dqaOGPLKX/FVwIOEb7q+hjHdnZ4v5pKZcNv5GG4urjujIoyA== +connected-react-router@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.6.1.tgz#f6b7717abf959393fab6756c8d43af1a57d622da" + integrity sha512-a/SE3HgpZABCxr083bfAMpgZwUzlv1RkmOV71+D4I77edoR/peg7uJMHOgqWnXXqGD7lo3Y2ZgUlXtMhcv8FeA== dependencies: immutable "^3.8.1" prop-types "^15.7.2" @@ -2505,6 +2776,13 @@ convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.5.0: dependencies: safe-buffer "~5.1.1" +convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -2530,13 +2808,13 @@ copy-props@^2.0.1: each-props "^1.3.0" is-plain-object "^2.0.1" -core-js-compat@^3.1.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.2.1.tgz#0cbdbc2e386e8e00d3b85dc81c848effec5b8150" - integrity sha512-MwPZle5CF9dEaMYdDeWm73ao/IflDH+FjeJCWEADcEgFSE9TLimFKwJsfmkwzI8eC0Aj0mgvMDjeQjrElkz4/A== +core-js-compat@^3.4.7: + version "3.6.4" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17" + integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA== dependencies: - browserslist "^4.6.6" - semver "^6.3.0" + browserslist "^4.8.3" + semver "7.0.0" core-js@3: version "3.2.1" @@ -2558,7 +2836,7 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cosmiconfig@^5.0.0, cosmiconfig@^5.2.0: +cosmiconfig@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== @@ -2568,6 +2846,17 @@ cosmiconfig@^5.0.0, cosmiconfig@^5.2.0: js-yaml "^3.13.1" parse-json "^4.0.0" +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + create-ecdh@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" @@ -2616,16 +2905,7 @@ create-react-context@^0.3.0: gud "^1.0.0" warning "^4.0.3" -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^6.0.5: +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -2676,23 +2956,23 @@ css-declaration-sorter@^4.0.1: postcss "^7.0.1" timsort "^0.3.0" -css-loader@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.2.0.tgz#bb570d89c194f763627fcf1f80059c6832d009b2" - integrity sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ== +css-loader@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.2.1.tgz#62849b45a414b7bde0bfba17325a026471040eae" + integrity sha512-q40kYdcBNzMvkIImCL2O+wk8dh+RGwPPV9Dfz3n7XtOYPXqe2Z6VgtvoxjkLHz02gmhepG9sOAJOUlx+3hHsBg== dependencies: camelcase "^5.3.1" cssesc "^3.0.0" icss-utils "^4.1.1" loader-utils "^1.2.3" normalize-path "^3.0.0" - postcss "^7.0.17" + postcss "^7.0.23" postcss-modules-extract-imports "^2.0.0" postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.1.0" + postcss-modules-scope "^2.1.1" postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.0" - schema-utils "^2.0.0" + postcss-value-parser "^4.0.2" + schema-utils "^2.6.0" css-select-base-adapter@^0.1.1: version "0.1.1" @@ -2850,13 +3130,6 @@ cuint@^0.2.2: resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -2912,7 +3185,7 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -decamelize-keys@^1.0.0: +decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= @@ -2920,7 +3193,7 @@ decamelize-keys@^1.0.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.1.0, decamelize@^1.1.1: +decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -2930,6 +3203,18 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +deep-equal@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -3050,13 +3335,6 @@ dir-glob@2.0.0: arrify "^1.0.1" path-type "^3.0.0" -dir-glob@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" - integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== - dependencies: - path-type "^3.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -3064,16 +3342,16 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dnd-core@^9.3.4: - version "9.3.4" - resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-9.3.4.tgz#56b5fdc165aa7d102506d3d5a08ec1fa789e0775" - integrity sha512-sDzBiGXgpj9bQhs8gtPWFIKMg4WY8ywI9RI81rRAUWI4oNj/Sm/ztjS67UjCvMa+fWoQ2WNIV3U9oDqeBN0+2g== +dnd-core@^9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-9.5.1.tgz#e9ec02d33529b68fa528865704d40ac4b14f2baf" + integrity sha512-/yEWFF2jg51yyB8uA2UbvBr9Qis0Oo/4p9cqHLEKZdxzHHVSPfq0a/ool8NG6dIS6Q4uN+oKGObY0rNWiopJDA== dependencies: "@types/asap" "^2.0.0" "@types/invariant" "^2.2.30" asap "^2.0.6" invariant "^2.2.4" - redux "^4.0.1" + redux "^4.0.4" dnode-protocol@~0.2.2: version "0.2.2" @@ -3083,15 +3361,14 @@ dnode-protocol@~0.2.2: jsonify "~0.0.0" traverse "~0.6.3" -dnode@^1.2.2: +"dnode@https://github.com/christianvuerings/dnode#e08e620b18c9086d47fe68e08328b19465c62fb7": version "1.2.2" - resolved "https://registry.yarnpkg.com/dnode/-/dnode-1.2.2.tgz#4ac3cfe26e292b3b39b8258ae7d94edc58132efa" - integrity sha1-SsPP4m4pKzs5uCWK59lO3FgTLvo= + resolved "https://github.com/christianvuerings/dnode#e08e620b18c9086d47fe68e08328b19465c62fb7" dependencies: dnode-protocol "~0.2.2" jsonify "~0.0.0" optionalDependencies: - weak "^1.0.0" + weak-napi "^1.0.3" doctrine@^2.1.0: version "2.1.0" @@ -3183,11 +3460,6 @@ dot-prop@^4.1.1: dependencies: is-obj "^1.0.0" -duplexer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= - duplexify@^3.4.2, duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -3219,6 +3491,11 @@ electron-to-chromium@^1.3.191: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.244.tgz#7ba5461fa320ab16540a31b1d0defb7ec29b16e4" integrity sha512-nEfPd2EKnFeLuZ/+JsRG3KixRQwWf2SPpp09ftNt5ouGhg408N759+oXvdXy57+TcM34ykfJYj2JMkc1O3R0lQ== +electron-to-chromium@^1.3.363: + version "1.3.367" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.367.tgz#48abffcaa6591051b612ae70ddc657763ede2662" + integrity sha512-GCHQreWs4zhKA48FNXCjvpV4kTnKoLu2PSAfKX394g34NPvTs2pPh1+jzWitNwhmOYI8zIqt36ulRVRZUgqlfA== + element-class@0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/element-class/-/element-class-0.2.2.tgz#9d3bbd0767f9013ef8e1c8ebe722c1402a60050e" @@ -3307,7 +3584,7 @@ error@^7.0.0: string-template "~0.2.1" xtend "~4.0.0" -es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.5.1, es-abstract@^1.7.0: +es-abstract@^1.12.0, es-abstract@^1.5.1, es-abstract@^1.7.0: version "1.13.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== @@ -3319,6 +3596,23 @@ es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.5.1, es-abstract@^1.7.0 is-regex "^1.0.4" object-keys "^1.0.12" +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1: + version "1.17.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" + integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.1.5" + is-regex "^1.0.5" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" + es-to-primitive@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" @@ -3328,6 +3622,15 @@ es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: version "0.10.50" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778" @@ -3391,20 +3694,20 @@ eslint-plugin-filenames@1.3.2: lodash.snakecase "4.1.1" lodash.upperfirst "4.3.1" -eslint-plugin-react@7.14.3: - version "7.14.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13" - integrity sha512-EzdyyBWC4Uz2hPYBiEJrKCUi2Fn+BJ9B/pJQcjw5X+x/H2Nm59S4MJIvL4O5NEE0+WbnQwEBxWY03oUk+Bc3FA== +eslint-plugin-react@7.18.0: + version "7.18.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.18.0.tgz#2317831284d005b30aff8afb7c4e906f13fa8e7e" + integrity sha512-p+PGoGeV4SaZRDsXqdj9OWcOrOpZn8gXoGPcIQTzo2IDMbAKhNDnME9myZWqO3Ic4R3YmwAZ1lDjWl2R2hMUVQ== dependencies: - array-includes "^3.0.3" + array-includes "^3.1.1" doctrine "^2.1.0" has "^1.0.3" - jsx-ast-utils "^2.1.0" - object.entries "^1.1.0" - object.fromentries "^2.0.0" - object.values "^1.1.0" + jsx-ast-utils "^2.2.3" + object.entries "^1.1.1" + object.fromentries "^2.0.2" + object.values "^1.1.1" prop-types "^15.7.2" - resolve "^1.10.1" + resolve "^1.14.2" eslint-scope@^4.0.3: version "4.0.3" @@ -3422,22 +3725,22 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" - integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q== +eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== dependencies: - eslint-visitor-keys "^1.0.0" + eslint-visitor-keys "^1.1.0" eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== -eslint@6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.4.0.tgz#5aa9227c3fbe921982b2eda94ba0d7fae858611a" - integrity sha512-WTVEzK3lSFoXUovDHEbkJqCVPEPwbhCq4trDktNI6ygs7aO41d4cDT0JFAT5MivzZeVLWlg7vHL+bgrQv/t3vA== +eslint@6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" + integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" @@ -3446,19 +3749,19 @@ eslint@6.4.0: debug "^4.0.1" doctrine "^3.0.0" eslint-scope "^5.0.0" - eslint-utils "^1.4.2" + eslint-utils "^1.4.3" eslint-visitor-keys "^1.1.0" - espree "^6.1.1" + espree "^6.1.2" esquery "^1.0.1" esutils "^2.0.2" file-entry-cache "^5.0.1" functional-red-black-tree "^1.0.1" glob-parent "^5.0.0" - globals "^11.7.0" + globals "^12.1.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^6.4.1" + inquirer "^7.0.0" is-glob "^4.0.0" js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" @@ -3467,7 +3770,7 @@ eslint@6.4.0: minimatch "^3.0.4" mkdirp "^0.5.1" natural-compare "^1.4.0" - optionator "^0.8.2" + optionator "^0.8.3" progress "^2.0.0" regexpp "^2.0.1" semver "^6.1.2" @@ -3477,13 +3780,13 @@ eslint@6.4.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de" - integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ== +espree@^6.1.2: + version "6.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.0.tgz#349fef01a202bbab047748300deb37fa44da79d7" + integrity sha512-Xs8airJ7RQolnDIbLtRutmfvSsAe0xqMMAantCN/GMoqf81TFbeI1T7Jpd56qYu1uuh32dOG5W/X9uO+ghPXzA== dependencies: - acorn "^7.0.0" - acorn-jsx "^5.0.2" + acorn "^7.1.0" + acorn-jsx "^5.2.0" eslint-visitor-keys "^1.1.0" esprima@^4.0.0: @@ -3491,17 +3794,17 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esprint@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/esprint/-/esprint-0.5.0.tgz#25975b855b9df625ce2e32655db6dff1a84bbe36" - integrity sha512-TpaXKPy6g1saDqMYwqppZC6C0wQpYQAnhms6829oVvP6XieUbGjQdcNgatGQMihin2bMgE90tmX+1OOPc5tuiw== +esprint@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/esprint/-/esprint-0.6.0.tgz#c8afb1ee8ad1fe8545d7366f6f6d19c086f4b85a" + integrity sha512-pmnhbskul594uRBbFUdOmIaeDuOK9b/a3t0TMZDdKkSZgekFeESH9/t3CVBebuhEKDl8im4i+YhPg1cUw65lgQ== dependencies: - dnode "^1.2.2" + dnode "https://github.com/christianvuerings/dnode#e08e620b18c9086d47fe68e08328b19465c62fb7" fb-watchman "^2.0.0" - glob "^7.1.1" - sane "^1.6.0" - worker-farm "^1.3.1" - yargs "^8.0.1" + glob "^7.1.4" + sane "^4.1.0" + worker-farm "^1.7.0" + yargs "^14.0.0" esquery@^1.0.1: version "1.0.1" @@ -3535,19 +3838,6 @@ event-emitter@^0.3.5: d "1" es5-ext "~0.10.14" -event-stream@3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" - integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= - dependencies: - duplexer "~0.1.1" - from "~0" - map-stream "~0.1.0" - pause-stream "0.0.11" - split "0.3" - stream-combiner "~0.0.4" - through "~2.3.1" - events@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" @@ -3568,20 +3858,18 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -exec-sh@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" - integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== - dependencies: - merge "^1.2.0" +exec-sh@^0.3.2: + version "0.3.4" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" + integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" + cross-spawn "^6.0.0" + get-stream "^4.0.0" is-stream "^1.1.0" npm-run-path "^2.0.0" p-finally "^1.0.0" @@ -3720,7 +4008,7 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= -fast-glob@^2.0.2, fast-glob@^2.2.6: +fast-glob@^2.0.2: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== @@ -3744,12 +4032,24 @@ fast-glob@^3.0.3: merge2 "^1.2.3" micromatch "^4.0.2" +fast-glob@^3.1.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" + integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fast-levenshtein@~2.0.4: +fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -3793,10 +4093,10 @@ figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== dependencies: escape-string-regexp "^1.0.5" @@ -3807,13 +4107,13 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" -file-loader@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-4.2.0.tgz#5fb124d2369d7075d70a9a5abecd12e60a95215e" - integrity sha512-+xZnaK5R8kBJrHK0/6HRlrKNamvVS5rjyuju+rnyxRGuwUJwpAMsVzUl5dz6rK8brkzjV6JpcFNjp6NqV0g1OQ== +file-loader@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-5.0.2.tgz#7f3d8b4ac85a5e8df61338cfec95d7405f971caa" + integrity sha512-QMiQ+WBkGLejKe81HU8SZ9PovsU/5uaLo0JdTCEXOYv7i7jfAjHZi1tcwp9tSASJPOmmHZtbdCervFmXMH/Dcg== dependencies: loader-utils "^1.2.3" - schema-utils "^2.0.0" + schema-utils "^2.5.0" file-uri-to-path@1.0.0: version "1.0.0" @@ -3875,13 +4175,6 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" -find-up@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -3889,6 +4182,14 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + findup-sync@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" @@ -4002,11 +4303,6 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" -from@~0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= - fs-copy-file-sync@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fs-copy-file-sync/-/fs-copy-file-sync-1.1.1.tgz#11bf32c096c10d126e5f6b36d06eece776062918" @@ -4067,10 +4363,10 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -fuse.js@3.4.5: - version "3.4.5" - resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.5.tgz#8954fb43f9729bd5dbcb8c08f251db552595a7a6" - integrity sha512-s9PGTaQIkT69HaeoTVjwGsLfb8V8ScJLx5XGFcKHg0MqLUH/UZ4EKOtqtXX9k7AFqCGxD1aJmYb8Q5VYDibVRQ== +fuse.js@3.4.6: + version "3.4.6" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.6.tgz#545c3411fed88bf2e27c457cab6e73e7af697a45" + integrity sha512-H6aJY4UpLFwxj1+5nAvufom5b2BT2v45P1MkPvdGIK8fWjQx/7o6tTT1+ALV0yawQvbmvCF0ufl2et8eJ7v7Cg== gauge@~2.7.3: version "2.7.4" @@ -4091,6 +4387,11 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-node-dimensions@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823" @@ -4101,10 +4402,24 @@ get-stdin@^7.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ== -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-symbol-from-current-process-h@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz#510af52eaef873f7028854c3377f47f7bb200265" + integrity sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw== + +get-uv-event-loop-napi-h@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz#42b0b06b74c3ed21fbac8e7c72845fdb7a200208" + integrity sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg== + dependencies: + get-symbol-from-current-process-h "^1.0.1" get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" @@ -4148,6 +4463,13 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" +glob-parent@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" + integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== + dependencies: + is-glob "^4.0.1" + glob-stream@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" @@ -4229,11 +4551,18 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" -globals@^11.1.0, globals@^11.7.0: +globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +globals@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13" + integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw== + dependencies: + type-fest "^0.8.1" + globby@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22" @@ -4248,6 +4577,18 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" +globby@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.0.tgz#56fd0e9f0d4f8fb0c456f1ab0dee96e1380bc154" + integrity sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^8.0.1: version "8.0.2" resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" @@ -4261,20 +4602,6 @@ globby@^8.0.1: pify "^3.0.0" slash "^1.0.0" -globby@^9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" - integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== - dependencies: - "@types/glob" "^7.1.1" - array-union "^1.0.2" - dir-glob "^2.2.2" - fast-glob "^2.2.6" - glob "^7.1.3" - ignore "^4.0.3" - pify "^4.0.1" - slash "^2.0.0" - globjoin@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" @@ -4287,7 +4614,7 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" -gonzales-pe@^4.2.3: +gonzales-pe@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.4.tgz#356ae36a312c46fe0f1026dd6cb539039f8500d2" integrity sha512-v0Ts/8IsSbh9n1OJRnSfa7Nlxi4AkXIsWB6vPept8FDbL4bXn3FNuxjYtO/nmBGu7GDkL9MFeGebeSu6l55EPQ== @@ -4352,16 +4679,16 @@ gulp-concat@2.6.1: through2 "^2.0.0" vinyl "^2.0.0" -gulp-livereload@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/gulp-livereload/-/gulp-livereload-4.0.1.tgz#cb438e62f24363e26b44ddf36fd37c274b8b15ee" - integrity sha512-BfjRd3gyJ9VuFqIOM6C3041P0FUc0T5MXjABWWHp4iDLmdnJ1fDZAQz514OID+ICXbgIW7942r9luommHBtrfQ== +gulp-livereload@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/gulp-livereload/-/gulp-livereload-4.0.2.tgz#fc8a75c7511cd65afd2202cbcdc8bb0f8dde377b" + integrity sha512-InmaR50Xl1xB1WdEk4mrUgGHv3VhhlRLrx7u60iY5AAer90FlK95KXitPcGGQoi28zrUJM189d/h6+V470Ncgg== dependencies: chalk "^2.4.1" debug "^3.1.0" - event-stream "3.3.4" fancy-log "^1.3.2" lodash.assign "^4.2.0" + readable-stream "^3.0.6" tiny-lr "^1.1.1" vinyl "^2.2.0" @@ -4467,6 +4794,11 @@ har-validator@~5.1.0: ajv "^6.5.5" har-schema "^2.0.0" +hard-rejection@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -4479,11 +4811,21 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= +has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -4553,7 +4895,19 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -history@4.9.0, history@^4.9.0: +history@4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + +history@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== @@ -4621,7 +4975,7 @@ html-minifier@^3.2.3: relateurl "0.2.x" uglify-js "3.4.x" -html-tags@^3.0.0: +html-tags@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== @@ -4678,11 +5032,6 @@ https-proxy-agent@^2.2.1: agent-base "^4.3.0" debug "^3.1.0" -humps@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" - integrity sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao= - iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -4719,12 +5068,12 @@ ignore@^3.3.5: resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== -ignore@^4.0.3, ignore@^4.0.6: +ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.6, ignore@^5.1.1: +ignore@^5.1.1, ignore@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== @@ -4757,6 +5106,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-fresh@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-from@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" @@ -4774,11 +5131,16 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indent-string@^3.0.0, indent-string@^3.2.0: +indent-string@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -4817,23 +5179,23 @@ ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@^6.4.1: - version "6.5.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== +inquirer@^7.0.0: + version "7.0.5" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.5.tgz#fb95b238ba19966c1a1f55db53c3f0ce5c9e4275" + integrity sha512-6Z5cP+LAO0rzNE7xWjWtT84jxKa5ScLEGLgegPXeO3dGeU8lNe5Ii7SlXH6KVtLGlDuaEhsvsFjrjWjw8j5lFg== dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" + ansi-escapes "^4.2.1" + chalk "^3.0.0" + cli-cursor "^3.1.0" cli-width "^2.0.0" external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.5.3" + string-width "^4.1.0" + strip-ansi "^6.0.0" through "^2.3.6" interpret@^1.1.0: @@ -4898,6 +5260,11 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4930,6 +5297,11 @@ is-callable@^1.1.4: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== +is-callable@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" + integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== + is-color-stop@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" @@ -5144,6 +5516,13 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" +is-regex@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" + integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== + dependencies: + has "^1.0.3" + is-regexp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" @@ -5166,6 +5545,11 @@ is-stream@^1.0.1, is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + is-svg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" @@ -5180,7 +5564,7 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.0" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= @@ -5369,10 +5753,10 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsx-ast-utils@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb" - integrity sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ== +jsx-ast-utils@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f" + integrity sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA== dependencies: array-includes "^3.0.3" object.assign "^4.1.0" @@ -5416,10 +5800,10 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== -known-css-properties@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.14.0.tgz#d7032b4334a32dc22e6e46b081ec789daf18756c" - integrity sha512-P+0a/gBzLgVlCnK8I7VcD0yuYJscmWn66wH9tlKsQnmVdg689tLEmziwB9PuazZYLkcm07fvWOKCJJqI55sD5Q== +known-css-properties@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.17.0.tgz#1c535f530ee8e9e3e27bb6a718285780e1d07326" + integrity sha512-Vi3nxDGMm/z+lAaCjvAR1u+7fiv+sG6gU/iYDj5QOF8h76ytK9EW/EKfF0NeTyiGBi8Jy6Hklty/vxISrLox3w== last-call-webpack-plugin@^3.0.0: version "3.0.0" @@ -5490,6 +5874,11 @@ linear-layout-vector@0.0.1: resolved "https://registry.yarnpkg.com/linear-layout-vector/-/linear-layout-vector-0.0.1.tgz#398114d7303b6ecc7fd6b273af7b8401d8ba9c70" integrity sha1-OYEU1zA7bsx/1rJzr3uEAdi6nHA= +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + livereload-js@^2.3.0: version "2.4.0" resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.4.0.tgz#447c31cf1ea9ab52fc20db615c5ddf678f78009c" @@ -5506,26 +5895,6 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" @@ -5550,14 +5919,6 @@ loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2 emojis-list "^2.0.0" json5 "^1.0.1" -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -5566,6 +5927,13 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" @@ -5616,7 +5984,7 @@ lodash.upperfirst@4.3.1: resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" integrity sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984= -lodash@4.17.15, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.3, lodash@^4.17.5: +lodash@4.17.15, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -5647,27 +6015,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3 dependencies: js-tokens "^3.0.0 || ^4.0.0" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - lower-case@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -5719,21 +6071,16 @@ map-obj@^1.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= -map-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" - integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= +map-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" + integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g== map-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg= -map-stream@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" - integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= - map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -5766,10 +6113,10 @@ math-random@^1.0.1: resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== -mathml-tag-names@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" - integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== +mathml-tag-names@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" + integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== md5.js@^1.3.4: version "1.3.5" @@ -5797,13 +6144,6 @@ mdn-data@~1.1.0: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA== -mem@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" - integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y= - dependencies: - mimic-fn "^1.0.0" - memoizee@0.4.X: version "0.4.14" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" @@ -5826,30 +6166,32 @@ memory-fs@^0.4.0, memory-fs@^0.4.1: errno "^0.1.3" readable-stream "^2.0.1" -meow@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" - integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== - dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - yargs-parser "^10.0.0" +meow@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-6.0.1.tgz#f9b3f912c9aa039142cebcf74315129f4cd1ce1c" + integrity sha512-kxGTFgT/b7/oSRSQsJ0qsT5IMU+bgZ1eAdSA3kIV7onkW0QWo/hL5RbGlMfvBjHJKPE1LaPX0kdecYFiqYWjUw== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.1.1" + decamelize-keys "^1.1.0" + hard-rejection "^2.0.0" + minimist-options "^4.0.1" + normalize-package-data "^2.5.0" + read-pkg-up "^7.0.0" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.8.1" + yargs-parser "^16.1.0" merge2@^1.2.3: version "1.2.4" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.4.tgz#c9269589e6885a60cf80605d9522d4b67ca646e3" integrity sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A== -merge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== +merge2@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" + integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== micromatch@^2.1.5: version "2.3.11" @@ -5889,7 +6231,7 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.0, micromatch@^4.0.2: +micromatch@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== @@ -5922,10 +6264,15 @@ mime@^2.3.1, mime@^2.4.4: resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +min-indent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256" + integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= mini-create-react-context@^0.3.0: version "0.3.2" @@ -5956,17 +6303,17 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@^3.0.2, minimatch@^3.0.4: +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" -minimist-options@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" - integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== +minimist-options@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.0.2.tgz#29c4021373ded40d546186725e57761e4b1984a7" + integrity sha512-seq4hpWkYSUh1y7NXxzucwAN9yVlBc3Upgdjz8vLCP97jG8kaOmzYrVH/m7tQ1NYD1wdtZbSLfdy4zFmRWuc/w== dependencies: arrify "^1.0.1" is-plain-obj "^1.1.0" @@ -6032,10 +6379,10 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: dependencies: minimist "0.0.8" -mobile-detect@1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/mobile-detect/-/mobile-detect-1.4.3.tgz#e436a3839f5807dd4d3cd4e081f7d3a51ffda2dd" - integrity sha512-UaahPNLllQsstHOEHAmVnTHCMQrAS9eL5Qgdi50QrYz6UgGk+Xziz2udz2GN6NYcyODcPLnasC7a7s6R2DjiaQ== +mobile-detect@1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/mobile-detect/-/mobile-detect-1.4.4.tgz#686c74e92d3cc06b09a9b3594b7b981494b137f6" + integrity sha512-vTgEjKjS89C5yHL5qWPpT6BzKuOVqABp+A3Szpbx34pIy3sngxlGaFpgHhfj6fKze1w0QKeOSDbU7SKu7wDvRQ== moment@2.24.0: version "2.24.0" @@ -6074,12 +6421,12 @@ mute-stdout@^1.0.0: resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331" integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg== -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.0.5, nan@^2.12.1: +nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -6137,6 +6484,11 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" +node-addon-api@^1.1.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.1.tgz#cf813cd69bb8d9100f6bdca6755fc268f54ac492" + integrity sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ== + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -6207,6 +6559,13 @@ node-releases@^1.1.25: dependencies: semver "^5.3.0" +node-releases@^1.1.50: + version "1.1.50" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.50.tgz#803c40d2c45db172d0410e4efec83aa8c6ad0592" + integrity sha512-lgAmPv9eYZ0bGwUYAKlr8MG6K4CvWliWqnkcT2P8mMAgVrH3lqfBPorFlxiG1pHQnqmavJZ9vbMXUTNyMLbrgQ== + dependencies: + semver "^6.3.0" + node.extend@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/node.extend/-/node.extend-2.0.2.tgz#b4404525494acc99740f3703c496b7d5182cc6cc" @@ -6223,7 +6582,7 @@ nopt@^4.0.1: abbrev "1" osenv "^0.1.4" -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: +normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -6353,7 +6712,17 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-keys@^1.0.11, object-keys@^1.0.12: +object-inspect@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + +object-is@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" + integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -6385,25 +6754,25 @@ object.defaults@^1.0.0, object.defaults@^1.1.0: for-own "^1.0.0" isobject "^3.0.0" -object.entries@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" - integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA== +object.entries@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b" + integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ== dependencies: define-properties "^1.1.3" - es-abstract "^1.12.0" + es-abstract "^1.17.0-next.1" function-bind "^1.1.1" has "^1.0.3" -object.fromentries@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab" - integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA== +object.fromentries@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" + integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== dependencies: - define-properties "^1.1.2" - es-abstract "^1.11.0" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" function-bind "^1.1.1" - has "^1.0.1" + has "^1.0.3" object.getownpropertydescriptors@^2.0.3: version "2.0.3" @@ -6454,6 +6823,16 @@ object.values@^1.1.0: function-bind "^1.1.1" has "^1.0.3" +object.values@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" + integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -6461,12 +6840,12 @@ once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: dependencies: wrappy "1" -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== dependencies: - mimic-fn "^1.0.0" + mimic-fn "^2.1.0" optimize-css-assets-webpack-plugin@5.0.3: version "5.0.3" @@ -6476,17 +6855,17 @@ optimize-css-assets-webpack-plugin@5.0.3: cssnano "^4.1.10" last-call-webpack-plugin "^3.0.0" -optionator@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= +optionator@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: deep-is "~0.1.3" - fast-levenshtein "~2.0.4" + fast-levenshtein "~2.0.6" levn "~0.3.0" prelude-ls "~1.1.2" type-check "~0.3.2" - wordwrap "~1.0.0" + word-wrap "~1.2.3" ordered-read-streams@^1.0.0: version "1.0.1" @@ -6519,15 +6898,6 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -os-locale@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" - integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA== - dependencies: - execa "^0.7.0" - lcid "^1.0.0" - mem "^1.1.0" - os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -6546,13 +6916,6 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - p-limit@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" @@ -6560,12 +6923,12 @@ p-limit@^2.0.0: dependencies: p-try "^2.0.0" -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= +p-limit@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" + integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== dependencies: - p-limit "^1.1.0" + p-try "^2.0.0" p-locate@^3.0.0: version "3.0.0" @@ -6574,6 +6937,13 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-map@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" @@ -6581,11 +6951,6 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -6677,6 +7042,16 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse-json@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" + integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + lines-and-columns "^1.1.6" + parse-node-version@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" @@ -6714,6 +7089,11 @@ path-exists@^3.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -6757,13 +7137,6 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -6776,13 +7149,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pause-stream@0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= - dependencies: - through "~2.3" - pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -6804,6 +7170,11 @@ picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== +picomatch@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" + integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -6943,18 +7314,18 @@ postcss-html@^0.36.0: dependencies: htmlparser2 "^3.10.0" -postcss-js@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-2.0.2.tgz#a5e75d3fb9d85b28e1d2bd57956c115665ea8542" - integrity sha512-HxXLw1lrczsbVXxyC+t/VIfje9ZeZhkkXE8KpFa3MEKfp2FyHDv29JShYY9eLhYrhLyWWHNIuwkktTfLXu2otw== +postcss-js@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-2.0.3.tgz#a96f0f23ff3d08cec7dc5b11bf11c5f8077cdab9" + integrity sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w== dependencies: camelcase-css "^2.0.1" - postcss "^7.0.17" + postcss "^7.0.18" -postcss-jsx@^0.36.1: - version "0.36.3" - resolved "https://registry.yarnpkg.com/postcss-jsx/-/postcss-jsx-0.36.3.tgz#c91113eae2935a1c94f00353b788ece9acae3f46" - integrity sha512-yV8Ndo6KzU8eho5mCn7LoLUGPkXrRXRjhMpX4AaYJ9wLJPv099xbtpbRQ8FrPnzVxb/cuMebbPR7LweSt+hTfA== +postcss-jsx@^0.36.3: + version "0.36.4" + resolved "https://registry.yarnpkg.com/postcss-jsx/-/postcss-jsx-0.36.4.tgz#37a68f300a39e5748d547f19a747b3257240bd50" + integrity sha512-jwO/7qWUvYuWYnpOb0+4bIIgJt7003pgU3P6nETBLaOyBXuTD55ho21xnals5nBrlpTIFodyd3/jBi6UO3dHvA== dependencies: "@babel/core" ">=7.2.2" @@ -7063,14 +7434,14 @@ postcss-minify-selectors@^4.0.2: postcss "^7.0.0" postcss-selector-parser "^3.0.0" -postcss-mixins@6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.2.2.tgz#3acea63271e2c75db62fb80bc1c29e1a609a4742" - integrity sha512-QqEZamiAMguYR6d2h73XXEHZgkxs03PlbU0PqgqtdCnbRlMLFNQgsfL/Td0rjIe2SwpLXOQyB9uoiLWa4GR7tg== +postcss-mixins@6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.2.3.tgz#021893ba455d04b5baa052bf196297ddd70e4af1" + integrity sha512-gfH5d09YilzDn/CLGFA9Lwv7GTezuyHgnAyXC8AfvhUMpl67ZTewhcpNuOgawClCOD+76XePE2IHO1xMgsOlvA== dependencies: globby "^8.0.1" - postcss "^7.0.17" - postcss-js "^2.0.2" + postcss "^7.0.21" + postcss-js "^2.0.3" postcss-simple-vars "^5.0.2" sugarss "^2.0.0" @@ -7091,10 +7462,10 @@ postcss-modules-local-by-default@^3.0.2: postcss-selector-parser "^6.0.2" postcss-value-parser "^4.0.0" -postcss-modules-scope@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb" - integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A== +postcss-modules-scope@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz#33d4fc946602eb5e9355c4165d68a10727689dba" + integrity sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ== dependencies: postcss "^7.0.6" postcss-selector-parser "^6.0.0" @@ -7107,13 +7478,13 @@ postcss-modules-values@^3.0.0: icss-utils "^4.0.0" postcss "^7.0.6" -postcss-nested@4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.1.2.tgz#8e0570f736bfb4be5136e31901bf2380b819a561" - integrity sha512-9bQFr2TezohU3KRSu9f6sfecXmf/x6RXDedl8CHF6fyuyVW7UqgNMRdWMHZQWuFY6Xqs2NYk+Fj4Z4vSOf7PQg== +postcss-nested@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.1.tgz#4bc2e5b35e3b1e481ff81e23b700da7f82a8b248" + integrity sha512-AMayXX8tS0HCp4O4lolp4ygj9wBn32DJWXvG6gCv+ZvJrEa00GUxJcJEEzMh87BIe6FrWdYkpR2cuyqHKrxmXw== dependencies: - postcss "^7.0.14" - postcss-selector-parser "^5.0.0" + postcss "^7.0.21" + postcss-selector-parser "^6.0.2" postcss-normalize-charset@^4.0.1: version "4.0.1" @@ -7247,13 +7618,13 @@ postcss-safe-parser@^4.0.1: dependencies: postcss "^7.0.0" -postcss-sass@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.3.5.tgz#6d3e39f101a53d2efa091f953493116d32beb68c" - integrity sha512-B5z2Kob4xBxFjcufFnhQ2HqJQ2y/Zs/ic5EZbCywCkxKd756Q40cIQ/veRDwSrw1BF6+4wUgmpm0sBASqVi65A== +postcss-sass@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.4.2.tgz#7d1f8ddf6960d329de28fb3ff43c9c42013646bc" + integrity sha512-hcRgnd91OQ6Ot9R90PE/khUDCJHG8Uxxd3F7Y0+9VHjBiJgNv7sK5FxyHMCBtoLmmkzVbSj3M3OlqUfLJpq0CQ== dependencies: - gonzales-pe "^4.2.3" - postcss "^7.0.1" + gonzales-pe "^4.2.4" + postcss "^7.0.21" postcss-scss@^2.0.0: version "2.0.0" @@ -7271,7 +7642,7 @@ postcss-selector-parser@^3.0.0, postcss-selector-parser@^3.1.0: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-selector-parser@^5.0.0, postcss-selector-parser@^5.0.0-rc.4: +postcss-selector-parser@^5.0.0-rc.4: version "5.0.0" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== @@ -7349,6 +7720,11 @@ postcss-value-parser@^4.0.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== +postcss-value-parser@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d" + integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg== + postcss@^6.0.23: version "6.0.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" @@ -7367,6 +7743,15 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.1 source-map "^0.6.1" supports-color "^6.1.0" +postcss@^7.0.18, postcss@^7.0.21, postcss@^7.0.23, postcss@^7.0.26: + version "7.0.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" + integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + prefix-style@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" @@ -7456,11 +7841,6 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - psl@^1.1.24: version "1.4.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" @@ -7523,10 +7903,10 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@6.9.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" + integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== qs@^6.4.0: version "6.8.0" @@ -7561,10 +7941,10 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -quick-lru@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" - integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== raf@^3.1.0: version "3.4.1" @@ -7623,7 +8003,7 @@ react-addons-shallow-compare@15.6.2: fbjs "^0.8.4" object-assign "^4.1.0" -react-async-script@1.1.1, react-async-script@^1.0.0: +react-async-script@1.1.1, react-async-script@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.1.1.tgz#f481c6c5f094bf4b94a9d52da0d0dda2e1a74bdf" integrity sha512-pmgS3O7JcX4YtH/Xy//NXylpD5CNb5T4/zqlVUV3HvcuyOanatvuveYoxl3X30ZSq/+q/+mSXcNS8xDVQJpSeA== @@ -7658,21 +8038,21 @@ react-custom-scrollbars@4.2.1: prop-types "^15.5.10" raf "^3.1.0" -react-dnd-html5-backend@9.3.4: - version "9.3.4" - resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-9.3.4.tgz#5d1f5ac608206d7b294b7407b9e1a336589eedd7" - integrity sha512-s+Xu0j7fHV9bLMSaOCuX76baQKcZfycAx0EzDmkxcFXPBiiFlI8l6rzwURdSJCjNcvLYXd8MLb4VkSNSq5ISZQ== +react-dnd-html5-backend@9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-9.5.1.tgz#e6a0aed3ece800c1abe004f9ed9991513e2e644c" + integrity sha512-wUdzjREwLqHxFkA6E+XDVL5IFjRDbBI3SHVKil9n3qrGT5dm2tA2oi1aIALdfMKsu00c+OXA9lz/LuKZCE9KXg== dependencies: - dnd-core "^9.3.4" + dnd-core "^9.5.1" -react-dnd@9.3.4: - version "9.3.4" - resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-9.3.4.tgz#ebab4b5b430b72f3580c058a29298054e1f9d2b8" - integrity sha512-UUtyoHFRrryMxVMEGYa3EdZIdibnys/ax7ZRs6CKpETHlnJQOFhHE3rpI+ManvKS0o3MFc1DZ+aoudAFtrOvFA== +react-dnd@9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-9.5.1.tgz#907e55c791d6c50cbed1a4021c14b989b86ac467" + integrity sha512-j2MvziPNLsxXkb3kIJzLvvOv/TQ4sysp6U4CmxAXd4C884dXm/9UGdB7K1wkTW3ZxVpI1K7XhKbX0JgNlPfLcA== dependencies: "@types/hoist-non-react-statics" "^3.3.1" "@types/shallowequal" "^1.1.1" - dnd-core "^9.3.4" + dnd-core "^9.5.1" hoist-non-react-statics "^3.3.0" shallowequal "^1.1.0" @@ -7694,23 +8074,23 @@ react-dom@16.8.6: prop-types "^15.6.2" scheduler "^0.13.6" -react-google-recaptcha@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-1.1.0.tgz#f33bef3e22e8c016820e80da48d573f516bb99e8" - integrity sha512-GMWZEsIKyBVG+iXfVMwtMVKFJATu5c+oguL/5i95H3Jb5d5CG4DY0W9t4QhdSSulgkXbZMgv0VSuGF/GV1ENTA== +react-google-recaptcha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8" + integrity sha512-4Y8awVnarn7+gdVpu8uvSmRJzzlMMoXqdhLoyToTOfVK6oM+NaChNI8NShnu75Q2YGHLvR1IA1FWZesuYHwn5w== dependencies: prop-types "^15.5.0" - react-async-script "^1.0.0" + react-async-script "^1.1.1" react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0: version "16.9.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== -react-lazyload@2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/react-lazyload/-/react-lazyload-2.6.2.tgz#6a1660de6e8653632797539189d19d64e924482c" - integrity sha512-zbFiwI3H7W0/Qvb6T/ew2NiGe2wj+soYNW7vv5Dte1eZuJDvvyUOHo8GpYfEeWoP5x4Rree2Hwop+lCISalBwg== +react-lazyload@2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/react-lazyload/-/react-lazyload-2.6.5.tgz#7a5ac001f0f8aeddc10c30e4ce318c10f13aa723" + integrity sha512-C/juO9l7dGS7jEARBLjM3oG7F1lL5bqajz/55sk3GFc0Ippd9vnSkdRxdiaE6gf5si3YxIow8dSJ+YuB2D/3vg== react-lifecycles-compat@^3.0.4: version "3.0.4" @@ -7726,22 +8106,23 @@ react-measure@1.4.7: prop-types "^15.5.4" resize-observer-polyfill "^1.4.1" -react-popper@1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.4.tgz#f0cd3b0d30378e1f663b0d79bcc8614221652ced" - integrity sha512-9AcQB29V+WrBKk6X7p0eojd1f25/oJajVdMZkywIoAV6Ag7hzE1Mhyeup2Q1QnvFRtGQFQvtqfhlEoDAPfKAVA== +react-popper@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324" + integrity sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww== dependencies: "@babel/runtime" "^7.1.2" create-react-context "^0.3.0" + deep-equal "^1.1.1" popper.js "^1.14.4" prop-types "^15.6.1" typed-styles "^0.0.7" warning "^4.0.2" -react-redux@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.1.tgz#ce6eee1b734a7a76e0788b3309bf78ff6b34fa0a" - integrity sha512-QsW0vcmVVdNQzEkrgzh2W3Ksvr8cqpAv5FhEk7tNEft+5pp7rXxAudTz3VOPawRkLIepItpkEIyLcN/VVXzjTg== +react-redux@7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79" + integrity sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w== dependencies: "@babel/runtime" "^7.5.5" hoist-non-react-statics "^3.3.0" @@ -7750,23 +8131,23 @@ react-redux@7.1.1: prop-types "^15.7.2" react-is "^16.9.0" -react-router-dom@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.1.tgz#ee66f4a5d18b6089c361958e443489d6bab714be" - integrity sha512-zaVHSy7NN0G91/Bz9GD4owex5+eop+KvgbxXsP/O+iW1/Ln+BrJ8QiIR5a6xNPtrdTvLkxqlDClx13QO1uB8CA== +react-router-dom@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" + integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew== dependencies: "@babel/runtime" "^7.1.2" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.0.1" + react-router "5.1.2" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.0.1.tgz#04ee77df1d1ab6cb8939f9f01ad5702dbadb8b0f" - integrity sha512-EM7suCPNKb1NxcTZ2LEOWFtQBQRQXecLxVpdsP4DW4PbbqYWeRiLyV/Tt1SdCrvT2jcyXAXmVTmzvSzrPR63Bg== +react-router@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418" + integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A== dependencies: "@babel/runtime" "^7.1.2" history "^4.9.0" @@ -7786,10 +8167,18 @@ react-side-effect@^1.0.2: dependencies: shallowequal "^1.0.1" -react-slider@0.11.2: - version "0.11.2" - resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-0.11.2.tgz#ae014e1454c3cdd5f28b5c2495b2a08abd9971e6" - integrity sha512-y49ZwJJ7OcPdihgt71xYI8GRdAzpFuSLQR8b+cKotutxqf8MAEPEtqvWKlg+3ZQRe5PMN6oWbIb7wEYDF8XhNQ== +react-slider@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-1.0.1.tgz#4cb212d31c35d804805e31f02ce37771e95d5d45" + integrity sha512-SI2anLzeKlFxnntoM93VXrf3ll9uL/TYjIJB6PsQVp4mDVt2VfWyGtMoUvK/ir/PnjuirNtF1pU3cG9XCezfBA== + +react-tabs@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.1.0.tgz#ecc50f034c1d6da2606fab9293055bbc861b382e" + integrity sha512-9RKc77HCPsjQDVPyZEw37g3JPtg26oSQ9o4mtaVXjJuLedDX5+TQcE+MRNKR+4aO3GMAY4YslCePGG1//MQ3Jg== + dependencies: + classnames "^2.2.0" + prop-types "^15.5.0" react-text-truncate@0.15.0: version "0.15.0" @@ -7836,21 +8225,14 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" - integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= +read-pkg-up@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" read-pkg@^1.0.0: version "1.1.0" @@ -7861,23 +8243,15 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" @@ -7911,6 +8285,15 @@ readable-stream@^1.0.33: isarray "0.0.1" string_decoder "~0.10.x" +readable-stream@^3.0.6: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -7927,13 +8310,13 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -redent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" - integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== dependencies: - indent-string "^3.0.0" - strip-indent "^2.0.0" + indent-string "^4.0.0" + strip-indent "^3.0.0" reduce-reducers@^0.4.3: version "0.4.3" @@ -7966,7 +8349,7 @@ redux-thunk@2.3.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== -redux@4.0.4, redux@^4.0.1: +redux@4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== @@ -7974,6 +8357,14 @@ redux@4.0.4, redux@^4.0.1: loose-envify "^1.4.0" symbol-observable "^1.2.0" +redux@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" + integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + regenerate-unicode-properties@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" @@ -8018,20 +8409,23 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp-tree@^0.1.6: - version "0.1.12" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.12.tgz#28eaaa6e66eeb3527c15108a3ff740d9e574e420" - integrity sha512-TsXZ8+cv2uxMEkLfgwO0E068gsNMLfuYwMMhiUxf0Kw2Vcgzq93vgl6wIlIYuPmfMqMjfQ9zAporiozqCnwLuQ== +regexp.prototype.flags@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== -regexpu-core@^4.5.4: - version "4.5.5" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.5.tgz#aaffe61c2af58269b3e516b61a73790376326411" - integrity sha512-FpI67+ky9J+cDizQUJlIlNZFKual/lUkFr1AG6zOCpwZ9cLrg8UUVakyUQJD7fCDIe9Z2nwTQJNPyonatNmDFQ== +regexpu-core@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.6.0.tgz#2037c18b327cfce8a6fea2a4ec441f2432afb8b6" + integrity sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg== dependencies: regenerate "^1.4.0" regenerate-unicode-properties "^8.1.0" @@ -8205,6 +8599,11 @@ require-main-filename@^1.0.1: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + require-nocache@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/require-nocache/-/require-nocache-1.0.0.tgz#a665d0b60a07e8249875790a4d350219d3c85fa3" @@ -8260,24 +8659,36 @@ resolve-pathname@^2.2.0: resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.4.0: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.4.0: version "1.12.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== dependencies: path-parse "^1.0.6" -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= +resolve@^1.14.2: + version "1.15.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" + integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== + dependencies: + path-parse "^1.0.6" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: - onetime "^2.0.0" + onetime "^5.1.0" signal-exit "^3.0.2" ret@~0.1.10: @@ -8334,10 +8745,15 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -run-async@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" - integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +run-async@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.0.tgz#e59054a5b86876cfae07f431d18cbaddc594f1e8" + integrity sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg== dependencies: is-promise "^2.1.0" @@ -8362,10 +8778,10 @@ run-sequence@2.2.1: fancy-log "^1.3.2" plugin-error "^0.1.2" -rxjs@^6.4.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" - integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== +rxjs@^6.5.3: + version "6.5.4" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" + integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== dependencies: tslib "^1.9.0" @@ -8396,18 +8812,20 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sane@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/sane/-/sane-1.7.0.tgz#b3579bccb45c94cf20355cc81124990dfd346e30" - integrity sha1-s1ebzLRclM8gNVzIESSZDf00bjA= +sane@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== dependencies: - anymatch "^1.3.0" - exec-sh "^0.2.0" + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" fb-watchman "^2.0.0" - minimatch "^3.0.2" + micromatch "^3.1.4" minimist "^1.1.1" walker "~1.0.5" - watch "~0.10.0" sax@^1.2.4, sax@~1.2.4: version "1.2.4" @@ -8439,13 +8857,13 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.1.0.tgz#940363b6b1ec407800a22951bdcc23363c039393" - integrity sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw== +schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.0: + version "2.6.4" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.4.tgz#a27efbf6e4e78689d91872ee3ccfa57d7bdd0f53" + integrity sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ== dependencies: - ajv "^6.1.0" - ajv-keywords "^3.1.0" + ajv "^6.10.2" + ajv-keywords "^3.4.1" seamless-immutable@^7.1.3: version "7.1.4" @@ -8474,6 +8892,11 @@ semver-greatest-satisfied-range@^1.1.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -8499,6 +8922,14 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +setimmediate-napi@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/setimmediate-napi/-/setimmediate-napi-1.0.6.tgz#43cd797ef25d66eb69c782170ea01898787b8720" + integrity sha512-sdNXN15Av1jPXuSal4Mk4tEAKn0+8lfF9Z50/negaQMrAIO9c1qM0eiCh8fT6gctp0RiCObk+6/Xfn5RMGdZoA== + dependencies: + get-symbol-from-current-process-h "^1.0.1" + get-uv-event-loop-napi-h "^1.0.5" + setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -8551,11 +8982,6 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= -slash@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" - integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -8689,13 +9115,6 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split@0.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" - integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8= - dependencies: - through "2" - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -8754,13 +9173,6 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-combiner@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" - integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ= - dependencies: - duplexer "~0.1.1" - stream-each@^1.1.0: version "1.2.3" resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" @@ -8817,7 +9229,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0: +"string-width@^1.0.2 || 2": version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -8825,7 +9237,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0: +string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== @@ -8843,6 +9255,31 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^5.2.0" +string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string.prototype.trimleft@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" + integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" + integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + string_decoder@0.10, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -8886,13 +9323,20 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.1.0, strip-ansi@^5.2.0: +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca" @@ -8913,20 +9357,17 @@ strip-bom@^2.0.0: dependencies: is-utf8 "^0.2.0" -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" strip-json-comments@^3.0.1: version "3.0.1" @@ -8938,13 +9379,13 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -style-loader@0.23.1: - version "0.23.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925" - integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg== +style-loader@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.0.1.tgz#aec6d4c61d0ed8d0a442faed741d4dfc6573888a" + integrity sha512-CnpEkSR1C+REjudiTWCv4+ssP7SCiuaQZJTZDWBRwTJoS90mdqkB8uOGMHKgVeUzpaU7IfLWoyQbvvs5Joj3Xw== dependencies: - loader-utils "^1.1.0" - schema-utils "^1.0.0" + loader-utils "^1.2.3" + schema-utils "^2.0.1" style-search@^0.1.0: version "0.1.0" @@ -8960,68 +9401,68 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" -stylelint-order@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-3.0.1.tgz#f1dd5e39345d04b684a6f04f1133cafa28606175" - integrity sha512-isVEJ1oUoVB7bb5pYop96KYOac4c+tLOqa5dPtAEwAwQUVSbi7OPFbfaCclcTjOlXicymasLpwhRirhFWh93yw== +stylelint-order@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-4.0.0.tgz#2a945c2198caac3ff44687d7c8582c81d044b556" + integrity sha512-bXV0v+jfB0+JKsqIn3mLglg1Dj2QCYkFHNfL1c+rVMEmruZmW5LUqT/ARBERfBm8SFtCuXpEdatidw/3IkcoiA== dependencies: - lodash "^4.17.14" - postcss "^7.0.17" + lodash "^4.17.15" + postcss "^7.0.26" postcss-sorting "^5.0.1" -stylelint@10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-10.1.0.tgz#1bc4c4ce878107e7c396b19226d91ba28268911a" - integrity sha512-OmlUXrgzEMLQYj1JPTpyZPR9G4bl0StidfHnGJEMpdiQ0JyTq0MPg1xkHk1/xVJ2rTPESyJCDWjG8Kbpoo7Kuw== +stylelint@13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.0.0.tgz#532007f7154c1a5ed14245d857a5884316f5111f" + integrity sha512-6sjgOJbM3iLhnUtmRO0J1vvxie9VnhIZX/2fCehjylv9Gl9u0ytehGCTm9Lhw2p1F8yaNZn5UprvhCB8C3g/Tg== dependencies: - autoprefixer "^9.5.1" + autoprefixer "^9.7.3" balanced-match "^1.0.0" - chalk "^2.4.2" - cosmiconfig "^5.2.0" + chalk "^3.0.0" + cosmiconfig "^6.0.0" debug "^4.1.1" execall "^2.0.0" file-entry-cache "^5.0.1" get-stdin "^7.0.0" global-modules "^2.0.0" - globby "^9.2.0" + globby "^11.0.0" globjoin "^0.1.4" - html-tags "^3.0.0" - ignore "^5.0.6" + html-tags "^3.1.0" + ignore "^5.1.4" import-lazy "^4.0.0" imurmurhash "^0.1.4" - known-css-properties "^0.14.0" + known-css-properties "^0.17.0" leven "^3.1.0" - lodash "^4.17.11" + lodash "^4.17.15" log-symbols "^3.0.0" - mathml-tag-names "^2.1.0" - meow "^5.0.0" - micromatch "^4.0.0" + mathml-tag-names "^2.1.1" + meow "^6.0.0" + micromatch "^4.0.2" normalize-selector "^0.2.0" - pify "^4.0.1" - postcss "^7.0.14" + postcss "^7.0.26" postcss-html "^0.36.0" - postcss-jsx "^0.36.1" + postcss-jsx "^0.36.3" postcss-less "^3.1.4" postcss-markdown "^0.36.0" postcss-media-query-parser "^0.2.3" postcss-reporter "^6.0.1" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^4.0.1" - postcss-sass "^0.3.5" + postcss-sass "^0.4.2" postcss-scss "^2.0.0" postcss-selector-parser "^3.1.0" postcss-syntax "^0.36.2" - postcss-value-parser "^3.3.1" + postcss-value-parser "^4.0.2" resolve-from "^5.0.0" - signal-exit "^3.0.2" slash "^3.0.0" specificity "^0.4.1" - string-width "^4.1.0" - strip-ansi "^5.2.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" style-search "^0.1.0" sugarss "^2.0.0" svg-tags "^1.0.0" - table "^5.2.3" + table "^5.4.6" + v8-compile-cache "^2.1.0" + write-file-atomic "^3.0.1" sugarss@^2.0.0: version "2.0.0" @@ -9049,6 +9490,13 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + sver-compat@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" @@ -9086,7 +9534,7 @@ symbol-observable@^1.2.0: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== -table@^5.2.3: +table@^5.2.3, table@^5.4.6: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== @@ -9166,7 +9614,7 @@ through2@^3.0.1: dependencies: readable-stream "2 || 3" -through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: +through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -9329,10 +9777,10 @@ traverse@~0.6.3: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= -trim-newlines@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" - integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= +trim-newlines@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" + integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== trim-right@^1.0.1: version "1.0.1" @@ -9388,6 +9836,21 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-fest@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + type@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/type/-/type-1.0.3.tgz#16f5d39f27a2d28d86e48f8981859e9d3296c179" @@ -9398,6 +9861,13 @@ typed-styles@^0.0.7: resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q== +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -9624,14 +10094,14 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= -url-loader@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-2.1.0.tgz#bcc1ecabbd197e913eca23f5e0378e24b4412961" - integrity sha512-kVrp/8VfEm5fUt+fl2E0FQyrpmOYgMEkBsv8+UDP1wFhszECq5JyGF33I7cajlVY90zRZ6MyfgKXngLvHYZX8A== +url-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-3.0.0.tgz#9f1f11b371acf6e51ed15a50db635e02eec18368" + integrity sha512-a84JJbIA5xTFTWyjjcPdnsu+41o/SNE8SpXMdUvXs6Q+LuhCD9E2+0VCiuDWqgo3GGXVlFHzArDmBpj9PgWn4A== dependencies: loader-utils "^1.2.3" mime "^2.4.4" - schema-utils "^2.0.0" + schema-utils "^2.5.0" url-parse@^1.4.3: version "1.4.7" @@ -9691,7 +10161,7 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== -v8-compile-cache@^2.0.3: +v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== @@ -9716,6 +10186,11 @@ value-equal@^0.4.0: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + value-or-function@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" @@ -9859,11 +10334,6 @@ warning@^4.0.2, warning@^4.0.3: dependencies: loose-envify "^1.0.0" -watch@~0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc" - integrity sha1-d3mLLaD5kQ1ZXxrOWwwiWFIfIdw= - watchpack@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" @@ -9873,13 +10343,14 @@ watchpack@^1.6.0: graceful-fs "^4.1.2" neo-async "^2.5.0" -weak@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/weak/-/weak-1.0.1.tgz#ab99aab30706959aa0200cb8cf545bb9cb33b99e" - integrity sha1-q5mqswcGlZqgIAy4z1RbucszuZ4= +weak-napi@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/weak-napi/-/weak-napi-1.0.3.tgz#ff4dfa818db1c509ba4166530b42414ef74cbba6" + integrity sha512-cyqeMaYA5qI7RoZKAKvIHwEROEKDNxK7jXj3u56nF2rGBh+HFyhYmBb1/wAN4RqzRmkYKVVKQyqHpBoJjqtGUA== dependencies: - bindings "^1.2.1" - nan "^2.0.5" + bindings "^1.3.0" + node-addon-api "^1.1.0" + setimmediate-napi "^1.0.3" webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: version "1.4.3" @@ -9904,7 +10375,36 @@ webpack-stream@5.2.1: vinyl "^2.1.0" webpack "^4.26.1" -webpack@4.39.3, webpack@^4.26.1: +webpack@4.41.2: + version "4.41.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.2.tgz#c34ec76daa3a8468c9b61a50336d8e3303dce74e" + integrity sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + acorn "^6.2.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.1" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.1" + watchpack "^1.6.0" + webpack-sources "^1.4.1" + +webpack@^4.26.1: version "4.39.3" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.39.3.tgz#a02179d1032156b713b6ec2da7e0df9d037def50" integrity sha512-BXSI9M211JyCVc3JxHWDpze85CvjC842EvpRsVTc/d15YJGlox7GIDd38kJgWrb3ZluyvIjgenbLDMBQPDcxYQ== @@ -9976,12 +10476,12 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" -wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -worker-farm@^1.3.1, worker-farm@^1.7.0: +worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== @@ -10004,11 +10504,30 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +write-file-atomic@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + write@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" @@ -10050,22 +10569,33 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== -yargs-parser@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== +yaml@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.7.2.tgz#f26aabf738590ab61efaca502358e48dc9f348b2" + integrity sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw== + dependencies: + "@babel/runtime" "^7.6.3" + +yargs-parser@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.0.tgz#cdd7a97490ec836195f59f3f4dbe5ea9e8f75f08" + integrity sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ== dependencies: - camelcase "^4.1.0" + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^16.1.0: + version "16.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1" + integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" yargs-parser@^5.0.0: version "5.0.0" @@ -10074,12 +10604,22 @@ yargs-parser@^5.0.0: dependencies: camelcase "^3.0.0" -yargs-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" - integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k= +yargs@^14.0.0: + version "14.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.2.tgz#2769564379009ff8597cdd38fba09da9b493c4b5" + integrity sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA== dependencies: - camelcase "^4.1.0" + cliui "^5.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^15.0.0" yargs@^7.1.0: version "7.1.0" @@ -10099,22 +10639,3 @@ yargs@^7.1.0: which-module "^1.0.0" y18n "^3.2.1" yargs-parser "^5.0.0" - -yargs@^8.0.1: - version "8.0.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" - integrity sha1-YpmpBVsc78lp/355wdkY3Osiw2A= - dependencies: - camelcase "^4.1.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - read-pkg-up "^2.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^7.0.0"

%A$XCD$MegLoIO_=GSkWR=CbX!X_YB6o>4z?gs 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�c56FE)~W1JS|w!K5J%i|3R-x{6|1M@5` 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 z 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 literal 2203 zcmb_c`8(7LAN`VT7&F6|vCIr(oneT?m_o?DGctp+FX^I;=vE3L)L3pr_FWVa*>b(M z%6hq6(PBvUu@56nk?Vclf8jmPbIy6rFXyMv=R9z*GZz+=5(EH1*wVs;eC!(krND_} z^lqmdn)cU=N)YB^)vn%o%zt!)wR97&05=($?=A&NW(!8_S)9y+~@Jx&l3|9 z3Bxr&ngds|EsT8nq}oY>DgledN+QI0z@ z?tj~7GMRJpbA3}|HJ{(c4prTMR~6q~7TH!nr_)_uK5%z;2l6SvBS!~&2d)P;K)@Ne z3rWd@$Yo0q#-Si6gfLQ+TZo?*0m0+(02=zJrvh*z0Di>zm8JESRlxI9?R0PIM3Z8n zKPJ>NdA#9nM{!6e8cl2JG9`C}{v?wU#cW(wRg1_c>Nc%1|Jo`GCC z1B8%(D_RgUHGrZDm@28rD$1ZRa7iQ}2LnX;0Rir#Uq?s#KRN3=+pC*n^HU!_etbRN z+y3{v!rAU;W1aMY7co8MiS0$dcNB)aEr@B)OHWUCsEM|#jHJvWCQ}*W(?gm(gvpn`1V%kaq`if8;Mc|}37!Lb=W%B#+ok@g~?VJD} zQP_{2<>|@c*4n~Ux`(c^yp{7F#WE?v84Uou36>^C7jAlFX2o6ivli>c4O}H&@?(uO zB@lh4nnP+sjs^91^Ilpz-R1_d`nKn=&EADCp3`;qMdvF0%NFNRL`5yU1{SM<4|mJ1 zOIx4b$%AP^L@JQcefMp)BgHjlqQP>?oN@|x-}kg=)a+>y$t<@F2D;>ZmHrW~V=VjH zOD$d8s@KMWQ_ixzYVH$JZF9NUFS9}axX`SsZ9G5kt!+jWAs`ES>uzq9kMmOsw4}0C zb8;}ZKE#qo4?p21_0szZ_6!E;Vs4J!D(4w@agAqoksrk?(8x1u!XY4FA)HgRwl>e} zwMuxaFqRr-q;5Bg=O?se?u&7!X!3Lyzi0DQkTw zhS-E%BVct`IbRM_Ph<79loI>lz@sGk#26_NM-+(3R*d>}{(z3g)7N|^ELJ1(mq6X= z3K2CuJg;9meuPGQ_u;lVPg5FKEVy&$V8cu%Nk)=w+?`(Dsszc*$g-H3&j#H|IvPp@cOFpSk8C96om*=A;RUoy!eR3@BMc;2SpiIOJw=&Ps=$P=y9A%By_8nNn>B3;@J1C7tbSAGM{fu znx9@C$ydCi`yoJT6KBHm=f*bXWp*asoJ0^w_&Wjb=#WLoc%Bh z_mmqP&OvrXLCFVfLetYt<$IY;&&}AS$!4!)QB7{s;2HO7vp*bMVX=Wr&^fTwRdX^G z0{dZ?`Q+RUECBC(j!uqD!o+1pB#1FI>Z@A)C5%lR zq8-vXU3`Pw@$MhtkW^0--Z*ZP;`;2+*`aUly%GK& z<7}>Bce%cXzRes)e6C^DMbs%)eDXN+GDtLh)(R%H zPPiY8FdGwl*H|uD{l2}TR1ec6yEKZRBs!v-dz|{)trw**FAWD-^e>g4_M@Bx^8Iho zV*wPnyNay5oAHpfy0H_Q@Mh4)Wu%fC8Avs+|FnRX3tl9Y`>gNZeqA&Y8>`?t+MP&g zY3k?g(TKiR$>^MPfN0eWs7HjU4zIjw`$1{dz-3%$s5fF>!O#5Y8gC$ObU>9tFAB9N z#lo8yDeT6Y7M*wTy3=CLjo$9G{U7n+bx%odQSu==6*tD_c5^C`6CJ^Sc5DT$4<;@7 zZT0?|sgC>Rr#W{w;^xt!NlN=-*iL7i!4VgoQylgsJ|OvcwE;_0JCoY;Ua|iI5$6Df diff --git a/Logo/512.png b/Logo/512.png index a937944188adebffe0ce2a79f7e1546582081be3..7d0c3c11636f5750d9973a6536b93798a0d499b3 100644 GIT binary patch literal 23550 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 literal 30123 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%0D0!lk`y4y^$bZ=1WEs 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 diff --git a/Logo/64.png b/Logo/64.png index 1b82050c38b029e9c39e110cd2d476dd4aceb12e..2d278cb382fad778648a38fa316164eb2dcaca53 100644 GIT binary patch delta 1999 zcmV;=2Qc{D72OYzB!34`OjJbx003iLTg$-KI{ zxwp5ou&|w(nR9M#-rnBf;o;fX*u=rX-{0TM$;sv9-Q3*7 z!otPF!@|J8!N0$bA|meY?nFF1?d|PGJ3H*`?Cb06rJ|y@dwXjxF6G+V#h93NI5<-_ zHtXomro4oNq-AAFIy!4KG+{9@ zaVsl#Dk|#a0t< z=iuPq&(F}Wug|Ni#+#d-nwrFwm6DB(tz=~7<>l13x5}ZR!H$lQi;JmkZAd^sOgTAA zIXPQ2G?XwfkR~SV>gwU(;O5-iyt=xdo14Osk-2|=uzzZ5scC7aSXfL&MVvrDdO10J zDJiFmNr*^DjzvXAK0cW` zI)gYk=H}+w#KgqG!MnM+sivl@Zf>buT%cK5rc_jzPELkLM}U`(q8m!q%eB zk$;BQKYfM~dfd@Ug-~n9V}^U?bhz!`XKeSZuh_378?;&nW&VG~ePfI@Qv{EZcuI~p zU(m+ruGg#@6}>7|ufn)$TW3((3h+VbQ;7olH?Z+5o3UikT7QF*^#-kMc&jeLjOI$b za3cgE>J%1#U52E}pQ_r)AZk}VEl0ev34dk}!Kb)~_rEj%B7+NV4Jkkx_d+$W2bM5G zN|61r{W>V1q1kaV42{nYA&E(B$pBwSvHzA3x5h0e!;;)xgJhng_p4H#0b=m^VgItV zZt_U#R)atGupMGJGsMf0{gM6F%>3__{Fp2){aIDXM=-0AB`Xm-p3R>m{e3+NGJVp0~b;Jwo!!+{AH8FWw02(4pISRt(mp->NYiahfX`yHilA@*}?;~ z*hf(dJnx%y+JhlhTUPsd+RWoRDt{=d553&NE=WfC2l?~4TkkYG+WlVC^nA1*s=wEb z`WKZib^2wU4KfSmW-@X_=83})TFgVVvHxVN$AJ|_dk(CS7P~qcZ_4bM*&ZIjLW>~Y zfubo30CJHFT0szaEPa`AnP6SHE31BwU)=A8dF*}2tXAZRQ~;2`Z#@cu4}W<9G^_=q z>i>N;L-cfR1oE^f80tY$0`>nK{FcH=-*)B&)6AyXZfk$}A%tP*hj=Un#q)QF1av@f z`nrn);C5lL7ivdB*P9w3wOde9CJ=F`{?0)XDQEHo$X+P4+N4B&cmb+$L@Yc@;6$sm zMLfV1{Rvc1e8&=(;aLU$jDMt7NVHZMfHZ(@YbFtXP1HcKq$Ojqxt1U7B?rP&M*K+XyK5B`(T)FGL zyrOO-1R%t;BXt!;)PG;*dJkJL6OH!2t6fXx`*1++@AMEwePV&70)fJdJ*y z53ub*_qCE~^L}w{9!DuO-UDhhs~1!nbj0N^et>zs9AT=MMt>83U!Ee|>Q3-m-FCPs zrr+g_Oq=+v?swrq8}YlbAu=zHs=*x@@XRdPOX_&zU5&59x-p*t}CUx~hS h;licpD@Tq0{{g)5;@bZxpLYNN002ovPDHLkV1n+72aW&$ literal 2780 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 diff --git a/Logo/800.png b/Logo/800.png index f802b07dd20eb2650e70062dc15943be8d19ffef..9aca60913820ad7bd9727d08f6077c1e8e08ca61 100644 GIT binary patch literal 38588 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