diff --git a/README.md b/README.md index 75e7bfdf8..b60fc1547 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Lidarr is a music collection manager for Usenet and BitTorrent users. It can mon ## Feature Requests -[![Feature Requests](http://feathub.com/lidarr/Lidarr?format=svg)](http://feathub.com/lidarr/Lidarr) +[![Feature Requests](http://feathub.com/mattman86/Lidarr?format=svg)](http://feathub.com/mattman86/Lidarr) ## Configuring Development Environment: diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..163e4f1f1 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,53 @@ +version: '0.2.0.{build}' + +assembly_info: + patch: true + file: 'src\NzbDrone.Common\Properties\SharedAssemblyInfo.cs' + assembly_version: '{version}' + assembly_file_version: '{version}' + assembly_informational_version: '{version}-rc1' + +environment: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + +install: + - git submodule update --init --recursive + +build_script: + - ps: ./build-appveyor.ps1 + +# test: off +test: + assemblies: + - '_tests\*Test.dll' + categories: + except: + - IntegrationTest + - AutomationTest + +artifacts: + - path: '_artifacts\*.zip' + - path: '_artifacts\*.exe' + - path: '_artifacts\*.tar.gz' + +cache: + - '%USERPROFILE%\.nuget\packages' + - node_modules + +pull_requests: + do_not_increment_build_number: true + +on_failure: + - ps: Get-ChildItem .\_artifacts\*.zip | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + - ps: Get-ChildItem .\_artifacts\*.exe | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + - ps: Get-ChildItem .\_artifacts\*.tar.gz | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + +only_commits: + files: + - src/ + - osx/ + - gulp/ + - logo/ + - setup/ + - appveyor.yml + - build-appveyor.cake \ No newline at end of file diff --git a/build-appveyor.cake b/build-appveyor.cake new file mode 100644 index 000000000..cce3fbd03 --- /dev/null +++ b/build-appveyor.cake @@ -0,0 +1,313 @@ +#addin "Cake.Npm" +#addin "SharpZipLib" +#addin "Cake.Compression" + +// Build variables +var outputFolder = "./_output"; +var outputFolderMono = outputFolder + "_mono"; +var outputFolderOsx = outputFolder + "_osx"; +var outputFolderOsxApp = outputFolderOsx + "_app"; +var testPackageFolder = "./_tests"; +var testSearchPattern = "*.Test/bin/x86/Release"; +var sourceFolder = "./src"; +var solutionFile = sourceFolder + "/NzbDrone.sln"; +var updateFolder = outputFolder + "/NzbDrone.Update"; +var updateFolderMono = outputFolderMono + "/NzbDrone.Update"; + +// Artifact variables +var artifactsFolder = "./_artifacts"; +var artifactsFolderWindows = artifactsFolder + "/windows"; +var artifactsFolderLinux = artifactsFolder + "/linux"; +var artifactsFolderOsx = artifactsFolder + "/osx"; +var artifactsFolderOsxApp = artifactsFolder + "/osx-app"; + +// Utility methods +public void RemoveEmptyFolders(string startLocation) { + foreach (var directory in System.IO.Directory.GetDirectories(startLocation)) + { + RemoveEmptyFolders(directory); + + if (System.IO.Directory.GetFiles(directory).Length == 0 && + System.IO.Directory.GetDirectories(directory).Length == 0) + { + DeleteDirectory(directory, false); + } + } +} + +public void CleanFolder(string path, bool keepConfigFiles) { + DeleteFiles(path + "/**/*.transform"); + + if (!keepConfigFiles) { + DeleteFiles(path + "/**/*.dll.config"); + } + + DeleteFiles(path + "/**/FluentValidation.resources.dll"); + DeleteFiles(path + "/**/App.config"); + + DeleteFiles(path + "/**/*.less"); + + DeleteFiles(path + "/**/*.vshost.exe"); + + DeleteFiles(path + "/**/*.dylib"); + + RemoveEmptyFolders(path); +} + +public void CreateMdbs(string path) { + foreach (var file in System.IO.Directory.EnumerateFiles(path, "*.pdb", System.IO.SearchOption.AllDirectories)) { + var actualFile = file.Substring(0, file.Length - 4); + + if (FileExists(actualFile + ".exe")) { + StartProcess("./tools/pdb2mdb/pdb2mdb.exe", new ProcessSettings() + .WithArguments(args => args.Append(actualFile + ".exe"))); + } + + if (FileExists(actualFile + ".dll")) { + StartProcess("./tools/pdb2mdb/pdb2mdb.exe", new ProcessSettings() + .WithArguments(args => args.Append(actualFile + ".dll"))); + } + } +} + +// Build Tasks +Task("Compile").Does(() => { + // Build + if (DirectoryExists(outputFolder)) { + DeleteDirectory(outputFolder, true); + } + + MSBuild(solutionFile, config => + config.UseToolVersion(MSBuildToolVersion.VS2015) + .WithTarget("Clean") + .SetVerbosity(Verbosity.Minimal)); + + NuGetRestore(solutionFile); + + MSBuild(solutionFile, config => + config.UseToolVersion(MSBuildToolVersion.VS2015) + .SetPlatformTarget(PlatformTarget.x86) + .SetConfiguration("Release") + .WithProperty("AllowedReferenceRelatedFileExtensions", new string[] { ".pdb" }) + .WithTarget("Build") + .SetVerbosity(Verbosity.Minimal)); + + CleanFolder(outputFolder, false); + + // Add JsonNet + DeleteFiles(outputFolder + "/Newtonsoft.Json.*"); + CopyFiles(sourceFolder + "/packages/Newtonsoft.Json.*/lib/net35/*.dll", outputFolder); + CopyFiles(sourceFolder + "/packages/Newtonsoft.Json.*/lib/net35/*.dll", updateFolder); + + // Remove Mono stuff + DeleteFile(outputFolder + "/Mono.Posix.dll"); +}); + +Task("Gulp").Does(() => { + NpmInstall(new NpmInstallSettings { + LogLevel = NpmLogLevel.Silent, + WorkingDirectory = "./", + Production = true + }); + + NpmRunScript("build"); +}); + +Task("PackageMono").Does(() => { + // Start mono package + if (DirectoryExists(outputFolderMono)) { + DeleteDirectory(outputFolderMono, true); + } + + CopyDirectory(outputFolder, outputFolderMono); + + // Create MDBs + CreateMdbs(outputFolderMono); + + // Remove PDBs + DeleteFiles(outputFolderMono + "/**/*.pdb"); + + // Remove service helpers + DeleteFiles(outputFolderMono + "/ServiceUninstall.*"); + DeleteFiles(outputFolderMono + "/ServiceInstall.*"); + + // Remove native windows binaries + DeleteFiles(outputFolderMono + "/sqlite3.*"); + DeleteFiles(outputFolderMono + "/MediaInfo.*"); + + // Adding NzbDrone.Core.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Core/NzbDrone.Core.dll.config", outputFolderMono + "/NzbDrone.Core.dll.config"); + + // Adding CurlSharp.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Common/CurlSharp.dll.config", outputFolderMono + "/CurlSharp.dll.config"); + + // Renaming Lidarr.Console.exe to Lidarr.exe + DeleteFiles(outputFolderMono + "/Lidarr.exe*"); + MoveFile(outputFolderMono + "/Lidarr.Console.exe", outputFolderMono + "/Lidarr.exe"); + MoveFile(outputFolderMono + "/Lidarr.Console.exe.config", outputFolderMono + "/Lidarr.exe.config"); + MoveFile(outputFolderMono + "/Lidarr.Console.exe.mdb", outputFolderMono + "/Lidarr.exe.mdb"); + + // Remove NzbDrone.Windows.* + DeleteFiles(outputFolderMono + "/NzbDrone.Windows.*"); + + // Adding NzbDrone.Mono to updatePackage + CopyFiles(outputFolderMono + "/NzbDrone.Mono.*", updateFolderMono); +}); + +Task("PackageOsx").Does(() => { + // Start osx package + if (DirectoryExists(outputFolderOsx)) { + DeleteDirectory(outputFolderOsx, true); + } + + CopyDirectory(outputFolderMono, outputFolderOsx); + + // Adding sqlite dylibs + CopyFiles(sourceFolder + "/Libraries/Sqlite/*.dylib", outputFolderOsx); + + // Adding MediaInfo dylib + CopyFiles(sourceFolder + "/Libraries/MediaInfo/*.dylib", outputFolderOsx); + + // Adding Startup script + CopyFile("./osx/Lidarr", outputFolderOsx + "/Lidarr"); +}); + +Task("PackageOsxApp").Does(() => { + // Start osx app package + if (DirectoryExists(outputFolderOsxApp)) { + DeleteDirectory(outputFolderOsxApp, true); + } + + CreateDirectory(outputFolderOsxApp); + + // Copy osx package files + CopyDirectory("./osx/Lidarr.app", outputFolderOsxApp + "/Lidarr.app"); + CopyDirectory(outputFolderOsx, outputFolderOsxApp + "/Lidarr.app/Contents/MacOS"); +}); + +Task("PackageTests").Does(() => { + // Start tests package + if (DirectoryExists(testPackageFolder)) { + DeleteDirectory(testPackageFolder, true); + } + + CreateDirectory(testPackageFolder); + + // Copy tests + CopyFiles(sourceFolder + "/" + testSearchPattern + "/*", testPackageFolder); + foreach (var directory in System.IO.Directory.GetDirectories(sourceFolder, "*.Test")) { + var releaseDirectory = directory + "/bin/x86/Release"; + if (DirectoryExists(releaseDirectory)) { + foreach (var releaseSubDirectory in System.IO.Directory.GetDirectories(releaseDirectory)) { + Information(System.IO.Path.GetDirectoryName(releaseSubDirectory)); + CopyDirectory(releaseSubDirectory, testPackageFolder + "/" + System.IO.Path.GetFileName(releaseSubDirectory)); + } + } + } + + // Install NUnit.ConsoleRunner + NuGetInstall("NUnit.ConsoleRunner", new NuGetInstallSettings { + Version = "3.2.0", + OutputDirectory = testPackageFolder + }); + + // Copy dlls + CopyFiles(outputFolder + "/*.dll", testPackageFolder); + + // Copy scripts + CopyFiles("./*.sh", testPackageFolder); + + // Create MDBs for tests + CreateMdbs(testPackageFolder); + + // Remove config + DeleteFiles(testPackageFolder + "/*.log.config"); + + // Clean + CleanFolder(testPackageFolder, true); + + // Adding NzbDrone.Core.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Core/NzbDrone.Core.dll.config", testPackageFolder + "/NzbDrone.Core.dll.config"); + + // Adding CurlSharp.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Common/CurlSharp.dll.config", testPackageFolder + "/CurlSharp.dll.config"); + + // Adding CurlSharp libraries + CopyFiles(sourceFolder + "/ExternalModules/CurlSharp/libs/i386/*", testPackageFolder); +}); + +Task("CleanupWindowsPackage").Does(() => { + // Remove mono + DeleteFiles(outputFolder + "/NzbDrone.Mono.*"); + + // Adding NzbDrone.Windows to updatePackage + CopyFiles(outputFolder + "/NzbDrone.Windows.*", updateFolder); +}); + +Task("Build") + .IsDependentOn("Compile") + .IsDependentOn("Gulp") + .IsDependentOn("PackageMono") + .IsDependentOn("PackageOsx") + .IsDependentOn("PackageOsxApp") + .IsDependentOn("PackageTests") + .IsDependentOn("CleanupWindowsPackage"); + +// Build Artifacts +Task("CleanArtifacts").Does(() => { + if (DirectoryExists(artifactsFolder)) { + DeleteDirectory(artifactsFolder, true); + } + + CreateDirectory(artifactsFolder); +}); + +Task("ArtifactsWindows").Does(() => { + CopyDirectory(outputFolder, artifactsFolderWindows + "/Lidarr"); +}); + +Task("ArtifactsWindowsInstaller").Does(() => { + InnoSetup("./setup/nzbdrone.iss", new InnoSetupSettings { + OutputDirectory = artifactsFolder, + ToolPath = "./setup/inno/ISCC.exe" + }); +}); + +Task("ArtifactsLinux").Does(() => { + CopyDirectory(outputFolderMono, artifactsFolderLinux + "/Lidarr"); +}); + +Task("ArtifactsOsx").Does(() => { + CopyDirectory(outputFolderOsx, artifactsFolderOsx + "/Lidarr"); +}); + +Task("ArtifactsOsxApp").Does(() => { + CopyDirectory(outputFolderOsxApp, artifactsFolderOsxApp); +}); + +Task("CompressArtifacts").Does(() => { + var prefix = ""; + + if (AppVeyor.IsRunningOnAppVeyor) { + prefix += AppVeyor.Environment.Repository.Branch.Replace("/", "-") + "."; + prefix += AppVeyor.Environment.Build.Version + "."; + } + + Zip(artifactsFolderWindows, artifactsFolder + "/Lidarr." + prefix + "windows.zip"); + GZipCompress(artifactsFolderLinux, artifactsFolder + "/Lidarr." + prefix + "linux.tar.gz"); + GZipCompress(artifactsFolderOsx, artifactsFolder + "/Lidarr." + prefix + "osx.tar.gz"); + Zip(artifactsFolderOsxApp, artifactsFolder + "/Lidarr." + prefix + "osx-app.zip"); +}); + +Task("Artifacts") + .IsDependentOn("CleanArtifacts") + .IsDependentOn("ArtifactsWindows") + .IsDependentOn("ArtifactsWindowsInstaller") + .IsDependentOn("ArtifactsLinux") + .IsDependentOn("ArtifactsOsx") + .IsDependentOn("ArtifactsOsxApp") + .IsDependentOn("CompressArtifacts"); + +// Run +RunTarget("Build"); +RunTarget("Artifacts"); diff --git a/build-appveyor.ps1 b/build-appveyor.ps1 new file mode 100644 index 000000000..fd3bea746 --- /dev/null +++ b/build-appveyor.ps1 @@ -0,0 +1,184 @@ +########################################################################## +# This is the Cake bootstrapper script for PowerShell. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +<# +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. +.PARAMETER Script +The build script to execute. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +.PARAMETER Experimental +Tells Cake to use the latest Roslyn release. +.PARAMETER WhatIf +Performs a dry run of the build script. +No tasks will be executed. +.PARAMETER Mono +Tells Cake to use the Mono scripting engine. +.PARAMETER SkipToolPackageRestore +Skips restoring of packages. +.PARAMETER ScriptArgs +Remaining arguments are added here. +.LINK +http://cakebuild.net +#> + +[CmdletBinding()] +Param( + [string]$Script = "build-appveyor.cake", + [string]$Target = "Default", + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity = "Verbose", + [switch]$Experimental, + [Alias("DryRun","Noop")] + [switch]$WhatIf, + [switch]$Mono, + [switch]$SkipToolPackageRestore, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$ScriptArgs +) + +[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null +function MD5HashFile([string] $filePath) +{ + if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) + { + return $null + } + + [System.IO.Stream] $file = $null; + [System.Security.Cryptography.MD5] $md5 = $null; + try + { + $md5 = [System.Security.Cryptography.MD5]::Create() + $file = [System.IO.File]::OpenRead($filePath) + return [System.BitConverter]::ToString($md5.ComputeHash($file)) + } + finally + { + if ($file -ne $null) + { + $file.Dispose() + } + } +} + +Write-Host "Preparing to run build script..." + +if(!$PSScriptRoot){ + $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +} + +$TOOLS_DIR = Join-Path $PSScriptRoot "tools-cake" +$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" +$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" +$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" +$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" +$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" + +# Should we use mono? +$UseMono = ""; +if($Mono.IsPresent) { + Write-Verbose -Message "Using the Mono based scripting engine." + $UseMono = "-mono" +} + +# Should we use the new Roslyn? +$UseExperimental = ""; +if($Experimental.IsPresent -and !($Mono.IsPresent)) { + Write-Verbose -Message "Using experimental version of Roslyn." + $UseExperimental = "-experimental" +} + +# Is this a dry run? +$UseDryRun = ""; +if($WhatIf.IsPresent) { + $UseDryRun = "-dryrun" +} + +# Make sure tools folder exists +if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { + Write-Verbose -Message "Creating tools directory..." + New-Item -Path $TOOLS_DIR -Type directory | out-null +} + +# Make sure that packages.config exist. +if (!(Test-Path $PACKAGES_CONFIG)) { + Write-Verbose -Message "Downloading packages.config..." + try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { + Throw "Could not download packages.config." + } +} + +# Try find NuGet.exe in path if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Trying to find nuget.exe in PATH..." + $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } + $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 + if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { + Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." + $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName + } +} + +# Try download NuGet.exe if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Downloading NuGet.exe..." + try { + (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) + } catch { + Throw "Could not download NuGet.exe." + } +} + +# Save nuget.exe path to environment to be available to child processed +$ENV:NUGET_EXE = $NUGET_EXE + +# Restore tools from NuGet? +if(-Not $SkipToolPackageRestore.IsPresent) { + Push-Location + Set-Location $TOOLS_DIR + + # Check for changes in packages.config and remove installed tools if true. + [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) + if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or + ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { + Write-Verbose -Message "Missing or changed package.config hash..." + Remove-Item * -Recurse -Exclude packages.config,nuget.exe + } + + Write-Verbose -Message "Restoring tools from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occured while restoring NuGet tools." + } + else + { + $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" + } + Write-Verbose -Message ($NuGetOutput | out-string) + Pop-Location +} + +# Make sure that Cake has been installed. +if (!(Test-Path $CAKE_EXE)) { + Throw "Could not find Cake.exe at $CAKE_EXE" +} + +# Start Cake +Write-Host "Running build script..." +Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" +exit $LASTEXITCODE \ No newline at end of file diff --git a/build.sh b/build.sh index e45c949e9..fec726920 100755 --- a/build.sh +++ b/build.sh @@ -181,7 +181,7 @@ PackageOsx() cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderOsx echo "Adding Startup script" - cp ./osx/Sonarr $outputFolderOsx + cp ./osx/Lidarr $outputFolderOsx echo "##teamcity[progressFinish 'Creating OS X Package']" } @@ -192,8 +192,8 @@ PackageOsxApp() rm -rf $outputFolderOsxApp mkdir $outputFolderOsxApp - cp -r ./osx/Sonarr.app $outputFolderOsxApp - cp -r $outputFolderOsx $outputFolderOsxApp/Sonarr.app/Contents/MacOS + cp -r ./osx/Lidarr.app $outputFolderOsxApp + cp -r $outputFolderOsx $outputFolderOsxApp/Lidarr.app/Contents/MacOS echo "##teamcity[progressFinish 'Creating OS X App Package']" } diff --git a/src/NzbDrone.Api/Music/AlbumResource.cs b/src/NzbDrone.Api/Music/AlbumResource.cs index a6d49d3bd..d3e243c66 100644 --- a/src/NzbDrone.Api/Music/AlbumResource.cs +++ b/src/NzbDrone.Api/Music/AlbumResource.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Api.Music { public class AlbumResource { - public int AlbumId { get; set; } + public string AlbumId { get; set; } public string AlbumName { get; set; } public bool Monitored { get; set; } public int Year { get; set; } diff --git a/src/NzbDrone.Api/Music/ArtistModule.cs b/src/NzbDrone.Api/Music/ArtistModule.cs index c598713d6..d7ef5fed3 100644 --- a/src/NzbDrone.Api/Music/ArtistModule.cs +++ b/src/NzbDrone.Api/Music/ArtistModule.cs @@ -71,7 +71,7 @@ namespace NzbDrone.Api.Music 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.ItunesId).GreaterThan(0).SetValidator(artistExistsValidator); + PostValidator.RuleFor(s => s.SpotifyId).NotEqual("").SetValidator(artistExistsValidator); PutValidator.RuleFor(s => s.Path).IsValidPath(); } diff --git a/src/NzbDrone.Api/Music/ArtistResource.cs b/src/NzbDrone.Api/Music/ArtistResource.cs index 89bc764a3..71cc14b85 100644 --- a/src/NzbDrone.Api/Music/ArtistResource.cs +++ b/src/NzbDrone.Api/Music/ArtistResource.cs @@ -19,9 +19,7 @@ namespace NzbDrone.Api.Music //View Only public string ArtistName { get; set; } - public int ItunesId { get; set; } - //public List AlternateTitles { get; set; } - //public string SortTitle { get; set; } + public string SpotifyId { get; set; } public string Overview { get; set; } public int AlbumCount @@ -30,7 +28,7 @@ namespace NzbDrone.Api.Music { if (Albums == null) return 0; - return Albums.Where(s => s.AlbumId > 0).Count(); // TODO: CHeck this condition + return Albums.Where(s => s.AlbumId != "").Count(); // TODO: CHeck this condition } } @@ -107,7 +105,7 @@ namespace NzbDrone.Api.Music //FirstAired = resource.FirstAired, //LastInfoSync = resource.LastInfoSync, //SeriesType = resource.SeriesType, - ItunesId = model.ItunesId, + SpotifyId = model.SpotifyId, ArtistSlug = model.ArtistSlug, RootFolderPath = model.RootFolderPath, @@ -151,16 +149,8 @@ namespace NzbDrone.Api.Music ArtistFolder = resource.ArtistFolder, Monitored = resource.Monitored, - - //UseSceneNumbering = resource.UseSceneNumbering, - //Runtime = resource.Runtime, - //TvdbId = resource.TvdbId, - //TvRageId = resource.TvRageId, - //TvMazeId = resource.TvMazeId, - //FirstAired = resource.FirstAired, //LastInfoSync = resource.LastInfoSync, - //SeriesType = resource.SeriesType, - ItunesId = resource.ItunesId, + SpotifyId = resource.SpotifyId, ArtistSlug = resource.ArtistSlug, RootFolderPath = resource.RootFolderPath, diff --git a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs index fa734300a..9efdda47c 100644 --- a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs +++ b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs @@ -17,7 +17,8 @@ namespace NzbDrone.Common.Cloud Services = new HttpRequestBuilder("http://services.lidarr.tv/v1/") .CreateFactory(); - Search = new HttpRequestBuilder("https://itunes.apple.com/{route}/") + Search = new HttpRequestBuilder("https://api.spotify.com/{version}/{route}/") // TODO: maybe use {version} + .SetSegment("version", "v1") .CreateFactory(); InternalSearch = new HttpRequestBuilder("https://itunes.apple.com/WebObjects/MZStore.woa/wa/{route}") //viewArtist or search diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs index 8a342e20c..0e2da7d1b 100644 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Datastore.Migration protected override void MainDbUpgrade() { Create.TableForModel("Artist") - .WithColumn("ItunesId").AsInt32().Unique() + .WithColumn("SpotifyId").AsString().Nullable().Unique() .WithColumn("ArtistName").AsString().Unique() .WithColumn("ArtistSlug").AsString().Nullable() //.Unique() .WithColumn("CleanTitle").AsString().Nullable() // Do we need this? @@ -37,8 +37,8 @@ namespace NzbDrone.Core.Datastore.Migration ; Create.TableForModel("Albums") - .WithColumn("AlbumId").AsInt32() - .WithColumn("ArtistId").AsInt32() + .WithColumn("AlbumId").AsString().Unique() + .WithColumn("ArtistId").AsInt32() // Should this be artistId (string) .WithColumn("Title").AsString() .WithColumn("Year").AsInt32() .WithColumn("Image").AsInt32() @@ -49,12 +49,13 @@ namespace NzbDrone.Core.Datastore.Migration Create.TableForModel("Tracks") .WithColumn("ItunesTrackId").AsInt32().Unique() - .WithColumn("AlbumId").AsInt32() + .WithColumn("AlbumId").AsString() .WithColumn("ArtistsId").AsString().Nullable() .WithColumn("TrackNumber").AsInt32() .WithColumn("Title").AsString().Nullable() .WithColumn("Ignored").AsBoolean().Nullable() .WithColumn("Explict").AsBoolean() + .WithColumn("Monitored").AsBoolean() .WithColumn("TrackExplicitName").AsString().Nullable() .WithColumn("TrackCensoredName").AsString().Nullable() .WithColumn("TrackFileId").AsInt32().Nullable() diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 3d2594bef..09163a47c 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -102,7 +102,7 @@ namespace NzbDrone.Core.Datastore .Relationships.AutoMapICollectionOrComplexProperties() .For("Tracks") .LazyLoad(condition: parent => parent.Id > 0, - query: (db, parent) => db.Query().Where(c => c.ItunesTrackId == parent.Id).ToList()) + query: (db, parent) => db.Query().Where(c => c.SpotifyTrackId == parent.Id).ToList()) .HasOne(file => file.Artist, file => file.AlbumId); Mapper.Entity().RegisterModel("Tracks") diff --git a/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs b/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs index 60a05febd..58c7ea61f 100644 --- a/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs +++ b/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs @@ -8,24 +8,24 @@ namespace NzbDrone.Core.Exceptions { public class ArtistNotFoundException : NzbDroneException { - public int ItunesId { get; set; } + public string SpotifyId { get; set; } - public ArtistNotFoundException(int itunesId) - : base(string.Format("Series with iTunesId {0} was not found, it may have been removed from iTunes.", itunesId)) + public ArtistNotFoundException(string spotifyId) + : base(string.Format("Artist with SpotifyId {0} was not found, it may have been removed from Spotify.", spotifyId)) { - ItunesId = itunesId; + SpotifyId = spotifyId; } - public ArtistNotFoundException(int itunesId, string message, params object[] args) + public ArtistNotFoundException(string spotifyId, string message, params object[] args) : base(message, args) { - ItunesId = itunesId; + SpotifyId = spotifyId; } - public ArtistNotFoundException(int itunesId, string message) + public ArtistNotFoundException(string spotifyId, string message) : base(message) { - ItunesId = itunesId; + SpotifyId = spotifyId; } } } diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 3ad7b909a..ef974f612 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -16,6 +16,7 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Commands; using NzbDrone.Core.Update.Commands; +using NzbDrone.Core.Music.Commands; namespace NzbDrone.Core.Jobs { @@ -64,9 +65,10 @@ namespace NzbDrone.Core.Jobs new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, - new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, + //new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName}, - new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, + new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshArtistCommand).FullName}, + new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, // TODO: Remove new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, new ScheduledTask{ Interval = 7*24*60, TypeName = typeof(BackupCommand).FullName}, diff --git a/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs index 4ae5a3420..76810655a 100644 --- a/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs +++ b/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs @@ -6,6 +6,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook { public interface IProvideArtistInfo { - Tuple> GetArtistInfo(int itunesId); + Tuple> GetArtistInfo(string spotifyId); } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumInfoResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumInfoResource.cs new file mode 100644 index 000000000..d442be7ec --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/AlbumInfoResource.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class AlbumInfoResource + { + public AlbumInfoResource() + { + + } + public string AlbumType { get; set; } // Might need to make this a separate class + public List Artists { get; set; } // Will always be length of 1 unless a compilation + public string Url { get; set; } // Link to the endpoint api to give full info for this object + public string Id { get; set; } // This is a unique Album ID. Needed for all future API calls + public List Images { get; set; } + public string Name { get; set; } // In case of a takedown, this may be empty + } + + +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistInfoResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistInfoResource.cs new file mode 100644 index 000000000..35f969001 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistInfoResource.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class ArtistInfoResource + { + public ArtistInfoResource() { } + + public List Genres { get; set; } + public string AristUrl { get; set; } + public string Id { get; set; } + public List Images { get; set; } + public string Name { get; set; } + + // We may need external_urls.spotify to external linking... + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs index 5113d5394..8d98d6890 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs @@ -5,63 +5,36 @@ using System.Text; namespace NzbDrone.Core.MetadataSource.SkyHook.Resource { - public class StorePlatformDataResource + + public class AristResultResource { - public StorePlatformDataResource() { } - public ArtistInfoResource Artist { get; set; } - //public Lockup lockup { get; set; } + public AristResultResource() + { + + } + + public List Items { get; set; } } - public class ArtistInfoResource + public class AlbumResultResource { - public ArtistInfoResource() { } - public Dictionary Results { get; set; } - - public bool HasArtistBio { get; set; } + public AlbumResultResource() + { - public string url { get; set; } - public string shortUrl { get; set; } - - public List artistContemporaries { get; set; } - public List genreNames { get; set; } - public bool hasSocialPosts { get; set; } - public string artistBio { get; set; } - public bool isGroup { get; set; } - public string id { get; set; } - public string bornOrFormed { get; set; } - public string name { get; set; } - public string latestAlbumContentId { get; set; } - public string nameRaw { get; set; } + } - //public string kind { get; set; } - //public List gallery { get; set; } - //public List genres { get; set; } - public List artistInfluencers { get; set; } - public List artistFollowers { get; set; } - //public string umcArtistImageUrl { get; set; } + public List Items { get; set; } } - public class AlbumResource - { - public AlbumResource() + public class TrackResultResource + { + public TrackResultResource() { } - public string ArtistName { get; set; } - public int ArtistId { get; set; } - public string CollectionName { get; set; } - public int CollectionId { get; set; } - public string PrimaryGenreName { get; set; } - public string ArtworkUrl100 { get; set; } - public string Country { get; set; } - public string CollectionExplicitness { get; set; } - public int TrackCount { get; set; } - public string Copyright { get; set; } - public DateTime ReleaseDate { get; set; } - + public List Items { get; set; } } - public class ArtistResource { public ArtistResource() @@ -69,10 +42,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource } - public int ResultCount { get; set; } - public List Results { get; set; } - //public string ArtistName { get; set; } - //public List Albums { get; set; } - public StorePlatformDataResource StorePlatformData { get; set; } + public AristResultResource Artists { get; set; } + public AristResultResource Albums { get; set; } } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs index 81a2f578e..518705757 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ImageResource.cs @@ -3,6 +3,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public class ImageResource { public string CoverType { get; set; } + + // Spotify Mapping public string Url { get; set; } + public int Height { get; set; } + public int Width { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackInfoResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackInfoResource.cs new file mode 100644 index 000000000..2f905637d --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TrackInfoResource.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class TrackInfoResource + { + public TrackInfoResource() + { + + } + + public int DiscNumber { get; set; } + public int DurationMs { get; set; } + public string Href { get; set; } + public string Id { get; set; } + public string Name { get; set; } + public int TrackNumber { get; set; } + public bool Explicit { get; set; } + public List Artists { get; set; } + + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 84600ae18..bf47348b1 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -22,16 +22,16 @@ namespace NzbDrone.Core.MetadataSource.SkyHook private readonly Logger _logger; private readonly IHttpRequestBuilderFactory _requestBuilder; - private readonly IHttpRequestBuilderFactory _internalRequestBuilder; public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, Logger logger) { _httpClient = httpClient; _requestBuilder = requestBuilder.Search; - _internalRequestBuilder = requestBuilder.InternalSearch; _logger = logger; } + + public Tuple> GetSeriesInfo(int tvdbSeriesId) { Console.WriteLine("[GetSeriesInfo] id:" + tvdbSeriesId); @@ -65,150 +65,136 @@ namespace NzbDrone.Core.MetadataSource.SkyHook public List SearchForNewSeries(string title) { - try - { - var lowerTitle = title.ToLowerInvariant(); - Console.WriteLine("Searching for " + lowerTitle); + // TODO: Remove this API + var tempList = new List(); + var tempSeries = new Series(); + tempSeries.Title = "AFI"; + tempList.Add(tempSeries); + return tempList; + } - //if (lowerTitle.StartsWith("tvdb:") || lowerTitle.StartsWith("tvdbid:")) - //{ - // var slug = lowerTitle.Split(':')[1].Trim(); - // int tvdbId; + public Tuple> GetArtistInfo(string spotifyId) + { - // if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || !int.TryParse(slug, out tvdbId) || tvdbId <= 0) - // { - // return new List(); - // } + _logger.Debug("Getting Artist with SpotifyId of {0}", spotifyId); - // try - // { - // return new List { GetSeriesInfo(tvdbId).Item1 }; - // } - // catch (SeriesNotFoundException) - // { - // return new List(); - // } - //} + ///v1/albums/{id} + // - // Majora: Temporarily, use iTunes to test. - var httpRequest = _requestBuilder.Create() - .AddQueryParam("entity", "album") - .AddQueryParam("term", title.ToLower().Trim()) - .Build(); + // We need to perform a direct lookup of the artist + var httpRequest = _requestBuilder.Create() + .SetSegment("route", "artists/" + spotifyId) + //.SetSegment("route", "search") + //.AddQueryParam("type", "artist,album") + //.AddQueryParam("q", spotifyId.ToString()) + .Build(); + + + + httpRequest.AllowAutoRedirect = true; + httpRequest.SuppressHttpError = true; + + var httpResponse = _httpClient.Get(httpRequest); + + + if (httpResponse.HasHttpError) + { + if (httpResponse.StatusCode == HttpStatusCode.NotFound) + { + throw new ArtistNotFoundException(spotifyId); + } + else + { + throw new HttpException(httpRequest, httpResponse); + } + } + Artist artist = new Artist(); + artist.ArtistName = httpResponse.Resource.Name; + artist.SpotifyId = httpResponse.Resource.Id; + artist.Genres = httpResponse.Resource.Genres; - Console.WriteLine("httpRequest: ", httpRequest); + artist = MapAlbums(artist); + - var httpResponse = _httpClient.Get>(httpRequest); + // TODO: implement tracks api call + return new Tuple>(artist, new List()); + } - //Console.WriteLine("Response: ", httpResponse.GetType()); - //_logger.Info("Response: ", httpResponse.Resource.ResultCount); + private Artist MapAlbums(Artist artist) + { - //_logger.Info("HTTP Response: ", httpResponse.Resource.ResultCount); - var tempList = new List(); - var tempSeries = new Series(); - tempSeries.Title = "AFI"; - tempList.Add(tempSeries); - return tempList; - - return httpResponse.Resource.SelectList(MapSeries); - } - catch (HttpException) + // Find all albums for the artist and all tracks for said album + ///v1/artists/{id}/albums + var httpRequest = _requestBuilder.Create() + .SetSegment("route", "artists/" + artist.SpotifyId + "/albums") + .Build(); + httpRequest.AllowAutoRedirect = true; + httpRequest.SuppressHttpError = true; + + var httpResponse = _httpClient.Get(httpRequest); + + if (httpResponse.HasHttpError) { - throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", title); + throw new HttpException(httpRequest, httpResponse); } - catch (Exception ex) + + List albums = new List(); + foreach(var albumResource in httpResponse.Resource.Items) { - _logger.Warn(ex, ex.Message); - throw new SkyHookException("Search for '{0}' failed. Invalid response received from SkyHook.", title); + Album album = new Album(); + album.AlbumId = albumResource.Id; + album.Title = albumResource.Name; + album.ArtworkUrl = albumResource.Images[0].Url; + album.Tracks = MapTracksToAlbum(album); + albums.Add(album); } - } - //public Artist GetArtistInfo(int itunesId) - //{ - // Console.WriteLine("[GetArtistInfo] id:" + itunesId); - // //https://itunes.apple.com/lookup?id=909253 - // //var httpRequest = _requestBuilder.Create() - // // .SetSegment("route", "lookup") - // // .AddQueryParam("id", itunesId.ToString()) - // // .Build(); - - // // TODO: Add special header, add Overview to Artist model - // var httpRequest = _requestBuilder.Create() - // .SetSegment("route", "viewArtist") - // .AddQueryParam("id", itunesId.ToString()) - // .Build(); - // httpRequest.Headers.Add("X-Apple-Store-Front", "143459-2,32 t:music3"); - - // httpRequest.AllowAutoRedirect = true; - // httpRequest.SuppressHttpError = true; - - // var httpResponse = _httpClient.Get(httpRequest); - - // if (httpResponse.HasHttpError) - // { - // if (httpResponse.StatusCode == HttpStatusCode.NotFound) - // { - // throw new ArtistNotFoundException(itunesId); - // } - // else - // { - // throw new HttpException(httpRequest, httpResponse); - // } - // } - - // Console.WriteLine("GetArtistInfo, GetArtistInfo"); - // return MapArtists(httpResponse.Resource)[0]; - //} + // TODO: We now need to get all tracks for each album - public Tuple> GetArtistInfo(int itunesId) - { - _logger.Debug("Getting Artist with iTunesID of {0}", itunesId); - var httpRequest1 = _requestBuilder.Create() - .SetSegment("route", "lookup") - .AddQueryParam("id", itunesId.ToString()) - .Build(); + artist.Albums = albums; + return artist; + } - var httpRequest2 = _internalRequestBuilder.Create() - .SetSegment("route", "viewArtist") - .AddQueryParam("id", itunesId.ToString()) - .Build(); - httpRequest2.Headers.Add("X-Apple-Store-Front", "143459-2,32 t:music3"); - httpRequest2.Headers.ContentType = "application/json"; + private List MapTracksToAlbum(Album album) + { + var httpRequest = _requestBuilder.Create() + .SetSegment("route", "albums/" + album.AlbumId + "/tracks") + .Build(); - httpRequest1.AllowAutoRedirect = true; - httpRequest1.SuppressHttpError = true; + httpRequest.AllowAutoRedirect = true; + httpRequest.SuppressHttpError = true; - var httpResponse = _httpClient.Get(httpRequest1); - + var httpResponse = _httpClient.Get(httpRequest); if (httpResponse.HasHttpError) { - if (httpResponse.StatusCode == HttpStatusCode.NotFound) - { - throw new ArtistNotFoundException(itunesId); - } - else - { - throw new HttpException(httpRequest1, httpResponse); - } + throw new HttpException(httpRequest, httpResponse); } - List artists = MapArtists(httpResponse.Resource); - List newArtists = new List(artists.Count); - int count = 0; - foreach (var artist in artists) + List tracks = new List(); + foreach(var trackResource in httpResponse.Resource.Items) { - newArtists.Add(AddOverview(artist)); - count++; + Track track = new Track(); + track.AlbumId = album.AlbumId; + //track.Album = album; // This will cause infinite loop when trying to serialize. + // TODO: Implement more track mapping + //track.Artist = trackResource.Artists + //track.ArtistId = album. + track.Explict = trackResource.Explicit; + track.Compilation = trackResource.Artists.Count > 1; + track.TrackNumber = trackResource.TrackNumber; + track.TrackExplicitName = trackResource.Name; + track.TrackCensoredName = trackResource.Name; + tracks.Add(track); } - // I don't know how we are getting tracks from iTunes yet. - return new Tuple>(newArtists[0], new List()); + return tracks; } - + + public List SearchForNewArtist(string title) { try @@ -220,16 +206,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook { var slug = lowerTitle.Split(':')[1].Trim(); - int itunesId; - - if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || !int.TryParse(slug, out itunesId) || itunesId <= 0) + if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace)) { return new List(); } try { - return new List { GetArtistInfo(itunesId).Item1 }; + return new List { GetArtistInfo(slug).Item1 }; } catch (ArtistNotFoundException) { @@ -239,8 +223,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var httpRequest = _requestBuilder.Create() .SetSegment("route", "search") - .AddQueryParam("entity", "album") - .AddQueryParam("term", title.ToLower().Trim()) + .AddQueryParam("type", "artist,album") + .AddQueryParam("q", title.ToLower().Trim()) .Build(); @@ -249,16 +233,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook List artists = MapArtists(httpResponse.Resource); - List newArtists = new List(artists.Count); - int count = 0; - foreach (var artist in artists) - { - newArtists.Add(AddOverview(artist)); - count++; - } - - return newArtists; + return artists; } catch (HttpException) { @@ -271,77 +247,52 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - private Artist AddOverview(Artist artist) - { - var httpRequest = _internalRequestBuilder.Create() - .SetSegment("route", "viewArtist") - .AddQueryParam("id", artist.ItunesId.ToString()) - .Build(); - httpRequest.Headers.Add("X-Apple-Store-Front", "143459-2,32 t:music3"); - httpRequest.Headers.ContentType = "application/json"; - var httpResponse = _httpClient.Get(httpRequest); - - if (!httpResponse.HasHttpError) - { - artist.Overview = httpResponse.Resource.StorePlatformData.Artist.Results[artist.ItunesId].artistBio; - } - - return artist; - } - private Artist MapArtistInfo(ArtistInfoResource resource) { // This expects ArtistInfoResource, thus just need to populate one artist Artist artist = new Artist(); - artist.Overview = resource.artistBio; - artist.ArtistName = resource.name; - foreach(var genre in resource.genreNames) - { - artist.Genres.Add(genre); - } + //artist.Overview = resource.artistBio; + //artist.ArtistName = resource.name; + //foreach(var genre in resource.genreNames) + //{ + // artist.Genres.Add(genre); + //} return artist; } private List MapArtists(ArtistResource resource) { - Album tempAlbum; + + List artists = new List(); - foreach (var album in resource.Results) + foreach(var artistResource in resource.Artists.Items) { - int index = artists.FindIndex(a => a.ItunesId == album.ArtistId); - tempAlbum = MapAlbum(album); + Artist artist = new Artist(); + artist.ArtistName = artistResource.Name; + artist.SpotifyId = artistResource.Id; + artist.Genres = artistResource.Genres; + //artist.ArtistSlug = a//TODO implement artistSlug mapping; + artists.Add(artist); + } - if (index >= 0) - { - artists[index].Albums.Add(tempAlbum); - } - else - { - Artist tempArtist = new Artist(); - tempArtist.ItunesId = album.ArtistId; - tempArtist.ArtistName = album.ArtistName; - tempArtist.Genres.Add(album.PrimaryGenreName); - tempArtist.Albums.Add(tempAlbum); - artists.Add(tempArtist); - } + // Maybe? Get all the albums for said artist - } return artists; } - private Album MapAlbum(AlbumResource albumQuery) - { - Album album = new Album(); - - album.AlbumId = albumQuery.CollectionId; - album.Title = albumQuery.CollectionName; - album.Year = albumQuery.ReleaseDate.Year; - album.ArtworkUrl = albumQuery.ArtworkUrl100; - album.Explicitness = albumQuery.CollectionExplicitness; - return album; - } + //private Album MapAlbum(AlbumResource albumQuery) + //{ + // Album album = new Album(); + + // album.AlbumId = albumQuery.CollectionId; + // album.Title = albumQuery.CollectionName; + // album.Year = albumQuery.ReleaseDate.Year; + // album.ArtworkUrl = albumQuery.ArtworkUrl100; + // album.Explicitness = albumQuery.CollectionExplicitness; + // return album; + //} private static Series MapSeries(ShowResource show) { diff --git a/src/NzbDrone.Core/Music/AddArtistService.cs b/src/NzbDrone.Core/Music/AddArtistService.cs index 3ca636d0b..07f995f69 100644 --- a/src/NzbDrone.Core/Music/AddArtistService.cs +++ b/src/NzbDrone.Core/Music/AddArtistService.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Music if (string.IsNullOrWhiteSpace(newArtist.Path)) { - var folderName = newArtist.ArtistName;// _fileNameBuilder.GetArtistFolder(newArtist); + var folderName = newArtist.ArtistName;// TODO: _fileNameBuilder.GetArtistFolder(newArtist); newArtist.Path = Path.Combine(newArtist.RootFolderPath, folderName); } @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Music throw new ValidationException(validationResult.Errors); } - _logger.Info("Adding Series {0} Path: [{1}]", newArtist, newArtist.Path); + _logger.Info("Adding Artist {0} Path: [{1}]", newArtist, newArtist.Path); _artistService.AddArtist(newArtist); return newArtist; @@ -75,15 +75,15 @@ namespace NzbDrone.Core.Music try { - tuple = _artistInfo.GetArtistInfo(newArtist.ItunesId); + tuple = _artistInfo.GetArtistInfo(newArtist.SpotifyId); } catch (SeriesNotFoundException) { - _logger.Error("iTunesId {1} was not found, it may have been removed from iTunes.", newArtist.ItunesId); + _logger.Error("SpotifyId {1} was not found, it may have been removed from Spotify.", newArtist.SpotifyId); throw new ValidationException(new List { - new ValidationFailure("iTunesId", "An artist with this ID was not found", newArtist.ItunesId) + new ValidationFailure("SpotifyId", "An artist with this ID was not found", newArtist.SpotifyId) }); } diff --git a/src/NzbDrone.Core/Music/AddArtistValidator.cs b/src/NzbDrone.Core/Music/AddArtistValidator.cs index a21e3bac5..ab789c2fc 100644 --- a/src/NzbDrone.Core/Music/AddArtistValidator.cs +++ b/src/NzbDrone.Core/Music/AddArtistValidator.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Music SeriesPathValidator seriesPathValidator, DroneFactoryValidator droneFactoryValidator, SeriesAncestorValidator seriesAncestorValidator, - ArtistSlugValidator seriesTitleSlugValidator) + ArtistSlugValidator artistTitleSlugValidator) { RuleFor(c => c.Path).Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Music .SetValidator(droneFactoryValidator) .SetValidator(seriesAncestorValidator); - RuleFor(c => c.ArtistSlug).SetValidator(seriesTitleSlugValidator);// TODO: Check if we are going to use a slug or artistName + RuleFor(c => c.ArtistSlug).SetValidator(artistTitleSlugValidator);// TODO: Check if we are going to use a slug or artistName } } } diff --git a/src/NzbDrone.Core/Music/Album.cs b/src/NzbDrone.Core/Music/Album.cs index c0c7fc19e..6d18a766b 100644 --- a/src/NzbDrone.Core/Music/Album.cs +++ b/src/NzbDrone.Core/Music/Album.cs @@ -14,10 +14,11 @@ namespace NzbDrone.Core.Music Images = new List(); } - public int AlbumId { get; set; } + public string AlbumId { get; set; } public string Title { get; set; } // NOTE: This should be CollectionName in API public int Year { get; set; } public int TrackCount { get; set; } + public List Tracks { get; set; } public int DiscCount { get; set; } public bool Monitored { get; set; } public List Images { get; set; } diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs index 457176bab..000aaf928 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Music } - public int ItunesId { get; set; } + public string SpotifyId { get; set; } public string ArtistName { get; set; } public string ArtistSlug { get; set; } public string CleanTitle { get; set; } @@ -32,44 +32,28 @@ namespace NzbDrone.Core.Music public bool ArtistFolder { get; set; } public DateTime? LastInfoSync { get; set; } public DateTime? LastDiskSync { get; set; } - public int Status { get; set; } // TODO: Figure out what this is, do we need it? public string Path { get; set; } public List Images { get; set; } public List Genres { get; set; } public int QualityProfileId { get; set; } - public string RootFolderPath { get; set; } public DateTime Added { get; set; } public LazyLoaded Profile { get; set; } public int ProfileId { get; set; } public List Albums { get; set; } public HashSet Tags { get; set; } - public AddSeriesOptions AddOptions { get; set; } - //public string SortTitle { get; set; } - //public SeriesStatusType Status { get; set; } - //public int Runtime { get; set; } - //public SeriesTypes SeriesType { get; set; } - //public string Network { get; set; } - //public bool UseSceneNumbering { get; set; } - //public string TitleSlug { get; set; } - //public int Year { get; set; } - //public Ratings Ratings { get; set; } - //public List Actors { get; set; } // MOve to album? - //public string Certification { get; set; } - //public DateTime? FirstAired { get; set; } - public override string ToString() { - return string.Format("[{0}][{1}]", ItunesId, ArtistName.NullSafe()); + return string.Format("[{0}][{1}]", SpotifyId, ArtistName.NullSafe()); } public void ApplyChanges(Artist otherArtist) { - ItunesId = otherArtist.ItunesId; + SpotifyId = otherArtist.SpotifyId; ArtistName = otherArtist.ArtistName; ArtistSlug = otherArtist.ArtistSlug; CleanTitle = otherArtist.CleanTitle; @@ -88,18 +72,11 @@ namespace NzbDrone.Core.Music ArtistFolder = otherArtist.ArtistFolder; AddOptions = otherArtist.AddOptions; - - //TODO: Implement - ItunesId = otherArtist.ItunesId; - Albums = otherArtist.Albums; Path = otherArtist.Path; ProfileId = otherArtist.ProfileId; - AlbumFolder = otherArtist.AlbumFolder; Monitored = otherArtist.Monitored; - - //SeriesType = otherArtist.SeriesType; RootFolderPath = otherArtist.RootFolderPath; Tags = otherArtist.Tags; AddOptions = otherArtist.AddOptions; diff --git a/src/NzbDrone.Core/Music/ArtistAddedHandler.cs b/src/NzbDrone.Core/Music/ArtistAddedHandler.cs new file mode 100644 index 000000000..b2da66db9 --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistAddedHandler.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music.Commands; +using NzbDrone.Core.Music.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class ArtistAddedHandler : IHandle + { + private readonly IManageCommandQueue _commandQueueManager; + + public ArtistAddedHandler(IManageCommandQueue commandQueueManager) + { + _commandQueueManager = commandQueueManager; + } + + public void Handle(ArtistAddedEvent message) + { + _commandQueueManager.Push(new RefreshArtistCommand(message.Artist.Id)); + } + } +} diff --git a/src/NzbDrone.Core/Music/ArtistRepository.cs b/src/NzbDrone.Core/Music/ArtistRepository.cs index f9e7f9da4..0da04ad0d 100644 --- a/src/NzbDrone.Core/Music/ArtistRepository.cs +++ b/src/NzbDrone.Core/Music/ArtistRepository.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Music { bool ArtistPathExists(string path); Artist FindByName(string cleanTitle); - Artist FindByItunesId(int iTunesId); + Artist FindById(string spotifyId); } public class ArtistRepository : BasicRepository, IArtistRepository @@ -24,9 +24,9 @@ namespace NzbDrone.Core.Music return Query.Where(c => c.Path == path).Any(); } - public Artist FindByItunesId(int iTunesId) + public Artist FindById(string spotifyId) { - return Query.Where(s => s.ItunesId == iTunesId).SingleOrDefault(); + return Query.Where(s => s.SpotifyId == spotifyId).SingleOrDefault(); } public Artist FindByName(string cleanName) diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs index 1397fad5d..bedf41f74 100644 --- a/src/NzbDrone.Core/Music/ArtistService.cs +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Music Artist GetArtist(int artistId); List GetArtists(IEnumerable artistIds); Artist AddArtist(Artist newArtist); - Artist FindByItunesId(int itunesId); + Artist FindById(string spotifyId); Artist FindByName(string title); Artist FindByTitleInexact(string title); void DeleteArtist(int artistId, bool deleteFiles); @@ -69,9 +69,9 @@ namespace NzbDrone.Core.Music _eventAggregator.PublishEvent(new ArtistDeletedEvent(artist, deleteFiles)); } - public Artist FindByItunesId(int itunesId) + public Artist FindById(string spotifyId) { - return _artistRepository.FindByItunesId(itunesId); + return _artistRepository.FindById(spotifyId); } public Artist FindByName(string title) @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Music if (storedAlbum != null && album.Monitored != storedAlbum.Monitored) { - _trackService.SetTrackMonitoredByAlbum(artist.Id, album.AlbumId, album.Monitored); + _trackService.SetTrackMonitoredByAlbum(artist.SpotifyId, album.AlbumId, album.Monitored); } } diff --git a/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs b/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs new file mode 100644 index 000000000..fdf3e56d6 --- /dev/null +++ b/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Commands +{ + public class RefreshArtistCommand : Command + { + public int? ArtistId { get; set; } + + public RefreshArtistCommand() + { + } + + public RefreshArtistCommand(int? artistId) + { + ArtistId = artistId; + } + + public override bool SendUpdatesToClient => true; + + public override bool UpdateScheduledTask => !ArtistId.HasValue; + } +} diff --git a/src/NzbDrone.Core/Music/Events/ArtistRefreshStartingEvent.cs b/src/NzbDrone.Core/Music/Events/ArtistRefreshStartingEvent.cs new file mode 100644 index 000000000..45d9a50c8 --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/ArtistRefreshStartingEvent.cs @@ -0,0 +1,18 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class ArtistRefreshStartingEvent : IEvent + { + public bool ManualTrigger { get; set; } + + public ArtistRefreshStartingEvent(bool manualTrigger) + { + ManualTrigger = manualTrigger; + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs b/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs new file mode 100644 index 000000000..99661c480 --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs @@ -0,0 +1,23 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class TrackInfoRefreshedEvent : IEvent + { + public Artist Artist { get; set; } + public ReadOnlyCollection Added { get; private set; } + public ReadOnlyCollection Updated { get; private set; } + + public TrackInfoRefreshedEvent(Artist artist, IList added, IList updated) + { + Artist = artist; + Added = new ReadOnlyCollection(added); + Updated = new ReadOnlyCollection(updated); + } + } +} diff --git a/src/NzbDrone.Core/Music/RefreshArtistService.cs b/src/NzbDrone.Core/Music/RefreshArtistService.cs new file mode 100644 index 000000000..af0717437 --- /dev/null +++ b/src/NzbDrone.Core/Music/RefreshArtistService.cs @@ -0,0 +1,173 @@ +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource.SkyHook; +using NzbDrone.Core.Music.Commands; +using NzbDrone.Core.Music.Events; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class RefreshArtistService : IExecute + { + private readonly IProvideArtistInfo _artistInfo; + private readonly IArtistService _artistService; + private readonly IRefreshTrackService _refreshTrackService; + private readonly IEventAggregator _eventAggregator; + //private readonly IDailySeriesService _dailySeriesService; + private readonly IDiskScanService _diskScanService; + //private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed; + private readonly Logger _logger; + + public RefreshArtistService(IProvideArtistInfo artistInfo, + IArtistService artistService, + IRefreshTrackService refreshTrackService, + IEventAggregator eventAggregator, + IDiskScanService diskScanService, + //ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed, + Logger logger) + { + _artistInfo = artistInfo; + _artistService = artistService; + _refreshTrackService = refreshTrackService; + _eventAggregator = eventAggregator; + _diskScanService = diskScanService; + //_checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed; + _logger = logger; + } + + private void RefreshArtistInfo(Artist artist) + { + _logger.ProgressInfo("Updating Info for {0}", artist.ArtistName); + + Tuple> tuple; + + try + { + tuple = _artistInfo.GetArtistInfo(artist.SpotifyId); + } + catch (ArtistNotFoundException) + { + _logger.Error("Artist '{0}' (SpotifyId {1}) was not found, it may have been removed from Spotify.", artist.ArtistName, artist.SpotifyId); + return; + } + + var artistInfo = tuple.Item1; + + if (artist.SpotifyId != artistInfo.SpotifyId) + { + _logger.Warn("Artist '{0}' (SpotifyId {1}) was replaced with '{2}' (SpotifyId {3}), because the original was a duplicate.", artist.ArtistName, artist.SpotifyId, artistInfo.ArtistName, artistInfo.SpotifyId); + artist.SpotifyId = artistInfo.SpotifyId; + } + + artist.ArtistName = artistInfo.ArtistName; + artist.ArtistSlug = artistInfo.ArtistSlug; + artist.Overview = artistInfo.Overview; + artist.Status = artistInfo.Status; + artist.CleanTitle = artistInfo.CleanTitle; + artist.LastInfoSync = DateTime.UtcNow; + artist.Images = artistInfo.Images; + //artist.Actors = artistInfo.Actors; + artist.Genres = artistInfo.Genres; + + try + { + artist.Path = new DirectoryInfo(artist.Path).FullName; + artist.Path = artist.Path.GetActualCasing(); + } + catch (Exception e) + { + _logger.Warn(e, "Couldn't update artist path for " + artist.Path); + } + + artist.Albums = UpdateAlbums(artist, artistInfo); + + _artistService.UpdateArtist(artist); + _refreshTrackService.RefreshTrackInfo(artist, tuple.Item2); + + _logger.Debug("Finished artist refresh for {0}", artist.ArtistName); + _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); + } + + private List UpdateAlbums(Artist artist, Artist artistInfo) + { + var albums = artistInfo.Albums.DistinctBy(s => s.AlbumId).ToList(); + + foreach (var album in albums) + { + var existingAlbum = artist.Albums.FirstOrDefault(s => s.AlbumId == album.AlbumId); + + //Todo: Should this should use the previous season's monitored state? + if (existingAlbum == null) + { + //if (album.SeasonNumber == 0) + //{ + // album.Monitored = false; + // continue; + //} + + _logger.Debug("New album ({0}) for artist: [{1}] {2}, setting monitored to true", album.Title, artist.SpotifyId, artist.ArtistName); + album.Monitored = true; + } + + else + { + album.Monitored = existingAlbum.Monitored; + } + } + + return albums; + } + + public void Execute(RefreshArtistCommand message) + { + _eventAggregator.PublishEvent(new ArtistRefreshStartingEvent(message.Trigger == CommandTrigger.Manual)); + + if (message.ArtistId.HasValue) + { + var artist = _artistService.GetArtist(message.ArtistId.Value); + RefreshArtistInfo(artist); + } + else + { + var allArtists = _artistService.GetAllArtists().OrderBy(c => c.ArtistName).ToList(); + + foreach (var artist in allArtists) + { + if (message.Trigger == CommandTrigger.Manual /*|| _checkIfArtistShouldBeRefreshed.ShouldRefresh(artist)*/) + { + try + { + RefreshArtistInfo(artist); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't refresh info for {0}", artist); + } + } + + else + { + try + { + _logger.Info("Skipping refresh of artist: {0}", artist.ArtistName); + //TODO: _diskScanService.Scan(artist); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't rescan artist {0}", artist); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Music/RefreshTrackService.cs b/src/NzbDrone.Core/Music/RefreshTrackService.cs new file mode 100644 index 000000000..bee0e8a5d --- /dev/null +++ b/src/NzbDrone.Core/Music/RefreshTrackService.cs @@ -0,0 +1,125 @@ +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public interface IRefreshTrackService + { + void RefreshTrackInfo(Artist artist, IEnumerable remoteTracks); + } + + public class RefreshTrackService : IRefreshTrackService + { + private readonly ITrackService _trackService; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public RefreshTrackService(ITrackService trackService, IEventAggregator eventAggregator, Logger logger) + { + _trackService = trackService; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public void RefreshTrackInfo(Artist artist, IEnumerable remoteTracks) + { + _logger.Info("Starting track info refresh for: {0}", artist); + var successCount = 0; + var failCount = 0; + + var existingTracks = _trackService.GetTrackByArtist(artist.SpotifyId); + var albums = artist.Albums; + + var updateList = new List(); + var newList = new List(); + var dupeFreeRemoteTracks = remoteTracks.DistinctBy(m => new { m.AlbumId, m.TrackNumber }).ToList(); + + foreach (var track in OrderTracks(artist, dupeFreeRemoteTracks)) + { + try + { + var trackToUpdate = GetTrackToUpdate(artist, track, existingTracks); + + if (trackToUpdate != null) + { + existingTracks.Remove(trackToUpdate); + updateList.Add(trackToUpdate); + } + else + { + trackToUpdate = new Track(); + trackToUpdate.Monitored = GetMonitoredStatus(track, albums); + newList.Add(trackToUpdate); + } + trackToUpdate.ArtistId = artist.SpotifyId; // TODO: Ensure LazyLoaded field gets updated. + trackToUpdate.TrackNumber = track.TrackNumber; + trackToUpdate.Title = track.Title ?? "Unknown"; + + // TODO: Implement rest of [RefreshTrackService] fields + + + + successCount++; + } + catch (Exception e) + { + _logger.Fatal(e, "An error has occurred while updating track info for artist {0}. {1}", artist, track); + failCount++; + } + } + + var allTracks = new List(); + allTracks.AddRange(newList); + allTracks.AddRange(updateList); + + // TODO: See if anything needs to be done here + //AdjustMultiEpisodeAirTime(artist, allTracks); + //AdjustDirectToDvdAirDate(artist, allTracks); + + _trackService.DeleteMany(existingTracks); + _trackService.UpdateMany(updateList); + _trackService.InsertMany(newList); + + _eventAggregator.PublishEvent(new TrackInfoRefreshedEvent(artist, newList, updateList)); + + if (failCount != 0) + { + _logger.Info("Finished track refresh for artist: {0}. Successful: {1} - Failed: {2} ", + artist.ArtistName, successCount, failCount); + } + else + { + _logger.Info("Finished track refresh for artist: {0}.", artist); + } + } + + private bool GetMonitoredStatus(Track track, IEnumerable albums) + { + if (track.TrackNumber == 0 /*&& track.AlbumId != 1*/) + { + return false; + } + + var album = albums.SingleOrDefault(c => c.AlbumId == track.AlbumId); + return album == null || album.Monitored; + } + + + private Track GetTrackToUpdate(Artist artist, Track track, List existingTracks) + { + return existingTracks.FirstOrDefault(e => e.AlbumId == track.AlbumId && e.TrackNumber == track.TrackNumber); + } + + private IEnumerable OrderTracks(Artist artist, List tracks) + { + return tracks.OrderBy(e => e.AlbumId).ThenBy(e => e.TrackNumber); + } + } +} + diff --git a/src/NzbDrone.Core/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs index 65e28231b..f1f563306 100644 --- a/src/NzbDrone.Core/Music/Track.cs +++ b/src/NzbDrone.Core/Music/Track.cs @@ -17,9 +17,10 @@ namespace NzbDrone.Core.Music public const string RELEASE_DATE_FORMAT = "yyyy-MM-dd"; - public int ItunesTrackId { get; set; } - public int AlbumId { get; set; } - public LazyLoaded ArtistsId { get; set; } + public int SpotifyTrackId { get; set; } + public string AlbumId { get; set; } + public LazyLoaded Artist { get; set; } + public string ArtistId { get; set; } public int CompilationId { get; set; } public bool Compilation { get; set; } public int TrackNumber { get; set; } @@ -28,11 +29,10 @@ namespace NzbDrone.Core.Music public bool Explict { get; set; } public string TrackExplicitName { get; set; } public string TrackCensoredName { get; set; } - public string Monitored { get; set; } - public int TrackFileId { get; set; } // JVM: Is this needed with TrackFile reference? + public bool Monitored { get; set; } + public int TrackFileId { get; set; } public DateTime? ReleaseDate { get; set; } - /*public int? SceneEpisodeNumber { get; set; } - public bool UnverifiedSceneNumbering { get; set; } + /* public Ratings Ratings { get; set; } // This might be aplicable as can be pulled from IDv3 tags public List Images { get; set; }*/ @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Music public override string ToString() { - return string.Format("[{0}]{1}", ItunesTrackId, Title.NullSafe()); + return string.Format("[{0}]{1}", SpotifyTrackId, Title.NullSafe()); } } } diff --git a/src/NzbDrone.Core/Music/TrackService.cs b/src/NzbDrone.Core/Music/TrackService.cs index 91bdeb5f7..b8bbbafa7 100644 --- a/src/NzbDrone.Core/Music/TrackService.cs +++ b/src/NzbDrone.Core/Music/TrackService.cs @@ -10,12 +10,12 @@ namespace NzbDrone.Core.Music { Track GetTrack(int id); List GetTracks(IEnumerable ids); - Track FindTrack(int artistId, int albumId, int trackNumber); - Track FindTrackByTitle(int artistId, int albumId, string releaseTitle); - List GetTrackByArtist(int artistId); - List GetTracksByAblum(int artistId, int albumId); - List GetTracksByAblumTitle(int artistId, string albumTitle); - List TracksWithFiles(int artistId); + Track FindTrack(string artistId, string albumId, int trackNumber); + Track FindTrackByTitle(string artistId, string albumId, string releaseTitle); + List GetTrackByArtist(string artistId); + List GetTracksByAlbum(string artistId, string albumId); + List GetTracksByAlbumTitle(string artistId, string albumTitle); + List TracksWithFiles(string artistId); PagingSpec TracksWithoutFiles(PagingSpec pagingSpec); List GeTracksByFileId(int trackFileId); void UpdateTrack(Track track); @@ -24,7 +24,7 @@ namespace NzbDrone.Core.Music void InsertMany(List tracks); void UpdateMany(List tracks); void DeleteMany(List tracks); - void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored); + void SetTrackMonitoredByAlbum(string artistId, string albumId, bool monitored); } public class TrackService : ITrackService @@ -34,12 +34,12 @@ namespace NzbDrone.Core.Music throw new NotImplementedException(); } - public Track FindTrack(int artistId, int albumId, int trackNumber) + public Track FindTrack(string artistId, string albumId, int trackNumber) { throw new NotImplementedException(); } - public Track FindTrackByTitle(int artistId, int albumId, string releaseTitle) + public Track FindTrackByTitle(string artistId, string albumId, string releaseTitle) { throw new NotImplementedException(); } @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Music throw new NotImplementedException(); } - public List GetTrackByArtist(int artistId) + public List GetTrackByArtist(string artistId) { throw new NotImplementedException(); } @@ -64,12 +64,12 @@ namespace NzbDrone.Core.Music throw new NotImplementedException(); } - public List GetTracksByAblum(int artistId, int albumId) + public List GetTracksByAlbum(string artistId, string albumId) { throw new NotImplementedException(); } - public List GetTracksByAblumTitle(int artistId, string albumTitle) + public List GetTracksByAlbumTitle(string artistId, string albumTitle) { throw new NotImplementedException(); } @@ -84,12 +84,12 @@ namespace NzbDrone.Core.Music throw new NotImplementedException(); } - public void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored) + public void SetTrackMonitoredByAlbum(string artistId, string albumId, bool monitored) { throw new NotImplementedException(); } - public List TracksWithFiles(int artistId) + public List TracksWithFiles(string artistId) { throw new NotImplementedException(); } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 678b2e8da..d263b8013 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -815,6 +815,8 @@ + + @@ -822,6 +824,7 @@ + @@ -851,14 +854,20 @@ + + + + + + diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index e3577527d..2f8b35588 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -24,7 +24,7 @@ namespace NzbDrone.Core.Parser.Model public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } - public int Album + public string Album { get { @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Parser.Model } } - public bool IsSpecial => Album == 0; + public bool IsSpecial => Album != ""; public override string ToString() { diff --git a/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs index 4a56bd072..3260895c5 100644 --- a/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Validation.Paths private readonly IArtistService _artistService; public ArtistExistsValidator(IArtistService artistService) - : base("This artist has already been added") + : base("This artist has already been added.") { _artistService = artistService; } @@ -21,9 +21,7 @@ namespace NzbDrone.Core.Validation.Paths { if (context.PropertyValue == null) return true; - var itunesId = Convert.ToInt32(context.PropertyValue.ToString()); - - return (!_artistService.GetAllArtists().Exists(s => s.ItunesId == itunesId)); + return (!_artistService.GetAllArtists().Exists(s => s.SpotifyId == context.PropertyValue.ToString())); } } } diff --git a/src/UI/AddSeries/SearchResultView.js b/src/UI/AddSeries/SearchResultView.js index aaef92a1f..8c4c70e7e 100644 --- a/src/UI/AddSeries/SearchResultView.js +++ b/src/UI/AddSeries/SearchResultView.js @@ -223,12 +223,12 @@ var view = Marionette.ItemView.extend({ self.close(); Messenger.show({ - message : 'Added: ' + self.model.get('title'), + message : 'Added: ' + self.model.get('artistName'), actions : { goToSeries : { label : 'Go to Artist', action : function() { - Backbone.history.navigate('/artist/' + self.model.get('titleSlug'), { trigger : true }); + Backbone.history.navigate('/artist/' + self.model.get('artistSlug'), { trigger : true }); } } }, diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs index f562800fc..19da3e335 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs @@ -8,7 +8,7 @@
@@ -50,6 +50,9 @@
{{> EpisodeProgressPartial }}
+
+ Path {{path}} +