diff --git a/Roadie.Library.Tests/SafeParserTests.cs b/Roadie.Library.Tests/SafeParserTests.cs index a277ecb..b783be6 100644 --- a/Roadie.Library.Tests/SafeParserTests.cs +++ b/Roadie.Library.Tests/SafeParserTests.cs @@ -68,5 +68,17 @@ namespace Roadie.Library.Tests Assert.NotNull(parsed); } + [Theory] + [InlineData("DEB4F298-5D22-4304-916E-F130B02864B7")] + [InlineData("12d65c61-1b7d-4c43-9aab-7d398a1a880e")] + [InlineData("A:8a951bc1-5ee5-4961-b72a-99d91d84c147")] + [InlineData("R:0327eea7-b1cb-4ae9-9eb1-b74b4416aefb")] + public void Parse_Guid(string input) + { + var parsed = SafeParser.ToGuid(input); + Assert.NotNull(parsed); + } + + } } diff --git a/RoadieApi/Controllers/SubsonicController.cs b/RoadieApi/Controllers/SubsonicController.cs index 7949b79..87187a8 100644 --- a/RoadieApi/Controllers/SubsonicController.cs +++ b/RoadieApi/Controllers/SubsonicController.cs @@ -5,9 +5,9 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Roadie.Api.Services; using Roadie.Library.Caching; +using Roadie.Library.Extensions; using Roadie.Library.Identity; using Roadie.Library.Models.ThirdPartyApi.Subsonic; -using System; using System.Net; using System.Threading.Tasks; @@ -32,31 +32,103 @@ namespace Roadie.Api.Controllers this.PlayActivityService = playActivityService; } + [HttpGet("getAlbum.view")] + [HttpPost("getAlbum.view")] + [ProducesResponseType(200)] + public async Task GetAlbum([FromQuery]Request request) + { + var result = await this.SubsonicService.GetAlbum(request, null); + return this.BuildResponse(request, result, "album"); + } + + [HttpGet("getAlbum.view")] + [HttpPost("getAlbum.view")] + [ProducesResponseType(200)] + public async Task GetSong([FromQuery]Request request, string id) + { + var result = await this.SubsonicService.GetAlbum(request, null); + return this.BuildResponse(request, result, "song"); + } + + [HttpGet("getArtist.view")] + [HttpPost("getArtist.view")] + [ProducesResponseType(200)] + public async Task GetArtist([FromQuery]Request request) + { + var result = await this.SubsonicService.GetArtist(request, null); + return this.BuildResponse(request, result, "artist"); + } + + [HttpGet("getAlbumInfo.view")] + [HttpPost("getAlbumInfo.view")] + [ProducesResponseType(200)] + public async Task GetAlbumInfo([FromQuery]Request request) + { + var result = await this.SubsonicService.GetAlbumInfo(request, null, AlbumInfoVersion.One); + return this.BuildResponse(request, result, "albumInfo"); + } + + [HttpGet("getAlbumInfo2.view")] + [HttpPost("getAlbumInfo2.view")] + [ProducesResponseType(200)] + public async Task GetAlbumInfo2([FromQuery]Request request) + { + var result = await this.SubsonicService.GetAlbumInfo(request, null, AlbumInfoVersion.Two); + return this.BuildResponse(request, result, "albumInfo"); + } + + [HttpGet("getVideos.view")] + [HttpPost("getVideos.view")] + [ProducesResponseType(200)] + public IActionResult GetVideos([FromQuery]Request request) + { + var result = this.SubsonicService.GetVideos(request); + return this.BuildResponse(request, result, "videos"); + } + + [HttpGet("getLyrics.view")] + [HttpPost("getLyrics.view")] + [ProducesResponseType(200)] + public IActionResult GetLyrics([FromQuery]Request request, string artist, string title) + { + var result = this.SubsonicService.GetLyrics(request, artist, title); + return this.BuildResponse(request, result, "lyrics "); + } + [HttpGet("getAlbumList.view")] [HttpPost("getAlbumList.view")] [ProducesResponseType(200)] public async Task GetAlbumList([FromQuery]Request request) { var result = await this.SubsonicService.GetAlbumList(request, null, AlbumListVersions.One); - return this.BuildResponse(request, result.Data, "albumList"); + return this.BuildResponse(request, result, "albumList"); } - [HttpGet("getRandomSongs.view")] - [HttpPost("getRandomSongs.view")] + [HttpGet("getAlbumList2.view")] + [HttpPost("getAlbumList2.view")] [ProducesResponseType(200)] - public async Task GetRandomSongs([FromQuery]Request request) + public async Task GetAlbumList2([FromQuery]Request request) { - var result = await this.SubsonicService.GetRandomSongs(request, null); - return this.BuildResponse(request, result.Data, "randomSongs"); + var result = await this.SubsonicService.GetAlbumList(request, null, AlbumListVersions.Two); + return this.BuildResponse(request, result, "albumList"); } - [HttpGet("getUser.view")] - [HttpPost("getUser.view")] + [HttpGet("getArtistInfo.view")] + [HttpPost("getArtistInfo.view")] [ProducesResponseType(200)] - public async Task GetUser([FromQuery]Request request, string username) + public async Task GetArtistInfo([FromQuery]Request request, int? count, bool? includeNotPresent) { - var result = await this.SubsonicService.GetUser(request, username); - return this.BuildResponse(request, result.Data, "user"); + var result = await this.SubsonicService.GetArtistInfo(request, count, includeNotPresent ?? false, ArtistInfoVersion.One); + return this.BuildResponse(request, result, "artistInfo"); + } + + [HttpGet("getArtistInfo2.view")] + [HttpPost("getArtistInfo2.view")] + [ProducesResponseType(200)] + public async Task GetArtistInfo2([FromQuery]Request request, int? count, bool? includeNotPresent) + { + var result = await this.SubsonicService.GetArtistInfo(request, count, includeNotPresent ?? false, ArtistInfoVersion.Two); + return this.BuildResponse(request, result, "artistInfo2"); } [HttpGet("getArtists.view")] @@ -65,25 +137,7 @@ namespace Roadie.Api.Controllers public async Task GetArtists([FromQuery]Request request) { var result = await this.SubsonicService.GetArtists(request, null); - return this.BuildResponse(request, result.Data, "artists"); - } - - [HttpGet("getStarred.view")] - [HttpPost("getStarred.view")] - [ProducesResponseType(200)] - public async Task GetStarred([FromQuery]Request request) - { - var result = await this.SubsonicService.GetStarred(request, null, StarredVersion.One); - return this.BuildResponse(request, result.Data, "starred"); - } - - [HttpGet("getStarred2.view")] - [HttpPost("getStarred2.view")] - [ProducesResponseType(200)] - public async Task GetStarred2([FromQuery]Request request) - { - var result = await this.SubsonicService.GetStarred(request, null, StarredVersion.Two); - return this.BuildResponse(request, result.Data, "starred"); + return this.BuildResponse(request, result, "artists"); } [HttpGet("getAvatar.view")] @@ -96,33 +150,6 @@ namespace Roadie.Api.Controllers return Redirect($"/images/user/{ user.RoadieId }/{this.RoadieSettings.ThumbnailImageSize.Width}/{this.RoadieSettings.ThumbnailImageSize.Height}"); } - [HttpGet("getAlbumList2.view")] - [HttpPost("getAlbumList2.view")] - [ProducesResponseType(200)] - public async Task GetAlbumList2([FromQuery]Request request) - { - var result = await this.SubsonicService.GetAlbumList(request, null, AlbumListVersions.Two); - return this.BuildResponse(request, result.Data, "albumList"); - } - - [HttpGet("getArtistInfo.view")] - [HttpPost("getArtistInfo.view")] - [ProducesResponseType(200)] - public async Task GetArtistInfo([FromQuery]Request request, string id, int? count, bool? includeNotPresent) - { - var result = await this.SubsonicService.GetArtistInfo(request, id, count, includeNotPresent ?? false, ArtistInfoVersion.One); - return this.BuildResponse(request, result.Data, "artistInfo"); - } - - [HttpGet("getArtistInfo2.view")] - [HttpPost("getArtistInfo2.view")] - [ProducesResponseType(200)] - public async Task GetArtistInfo2([FromQuery]Request request, string id, int? count, bool? includeNotPresent) - { - var result = await this.SubsonicService.GetArtistInfo(request, id, count, includeNotPresent ?? false, ArtistInfoVersion.Two); - return this.BuildResponse(request, result.Data, "artistInfo2"); - } - [HttpGet("getCoverArt.view")] [HttpPost("getCoverArt.view")] [ProducesResponseType(200)] @@ -151,74 +178,16 @@ namespace Roadie.Api.Controllers public async Task GetGenres([FromQuery]Request request) { var result = await this.SubsonicService.GetGenres(request); - return this.BuildResponse(request, result.Data, "genres"); + return this.BuildResponse(request, result, "genres"); } [HttpGet("getIndexes.view")] [HttpPost("getIndexes.view")] [ProducesResponseType(200)] - public async Task GetIndexes([FromQuery]Request request, string musicFolderId = null, long? ifModifiedSince = null) + public async Task GetIndexes([FromQuery]Request request, long? ifModifiedSince = null) { - var result = await this.SubsonicService.GetIndexes(request, null, musicFolderId, ifModifiedSince); - return this.BuildResponse(request, result.Data, "indexes"); - } - - [HttpGet("getMusicDirectory.view")] - [HttpPost("getMusicDirectory.view")] - [ProducesResponseType(200)] - public async Task GetMusicDirectory([FromQuery]Request request, string id) - { - var result = await this.SubsonicService.GetMusicDirectory(request, null, id); - return this.BuildResponse(request, result.Data, "directory"); - } - - [HttpGet("getMusicFolders.view")] - [HttpPost("getMusicFolders.view")] - [ProducesResponseType(200)] - public async Task GetMusicFolders([FromQuery]Request request) - { - var result = await this.SubsonicService.GetMusicFolders(request); - return this.BuildResponse(request, result.Data, "musicFolders"); - } - - [HttpGet("getPlaylist.view")] - [HttpPost("getPlaylist.view")] - [ProducesResponseType(200)] - public async Task GetPlaylist([FromQuery]Request request, string id) - { - var result = await this.SubsonicService.GetPlaylist(request, null, id); - return this.BuildResponse(request, result.Data, "playlist"); - } - - [HttpGet("getPlaylists.view")] - [HttpPost("getPlaylists.view")] - [ProducesResponseType(200)] - public async Task GetPlaylists([FromQuery]Request request, string username) - { - var result = await this.SubsonicService.GetPlaylists(request, null, username); - return this.BuildResponse(request, result.Data, "playlists"); - } - - [HttpGet("getPodcasts.view")] - [HttpPost("getPodcasts.view")] - [ProducesResponseType(200)] - public async Task GetPodcasts([FromQuery]Request request, bool includeEpisodes) - { - var result = await this.SubsonicService.GetPodcasts(request); - return this.BuildResponse(request, result.Data, "podcasts"); - } - - [HttpGet("ping.view")] - [HttpPost("ping.view")] - [ProducesResponseType(200)] - public IActionResult Ping([FromQuery]Request request) - { - if(request.IsJSONRequest) - { - var result = this.SubsonicService.Ping(request); - return this.BuildResponse(request, result.Data); - } - return Content("", "application/xml"); + var result = await this.SubsonicService.GetIndexes(request, null, ifModifiedSince); + return this.BuildResponse(request, result, "indexes"); } [HttpGet("getLicense.view")] @@ -227,7 +196,101 @@ namespace Roadie.Api.Controllers public IActionResult GetLicense([FromQuery]Request request) { var result = this.SubsonicService.GetLicense(request); - return this.BuildResponse(request, result.Data, "license"); + return this.BuildResponse(request, result, "license"); + } + + [HttpGet("getMusicDirectory.view")] + [HttpPost("getMusicDirectory.view")] + [ProducesResponseType(200)] + public async Task GetMusicDirectory([FromQuery]Request request) + { + var result = await this.SubsonicService.GetMusicDirectory(request, null); + return this.BuildResponse(request, result, "directory"); + } + + [HttpGet("getMusicFolders.view")] + [HttpPost("getMusicFolders.view")] + [ProducesResponseType(200)] + public async Task GetMusicFolders([FromQuery]Request request) + { + var result = await this.SubsonicService.GetMusicFolders(request); + return this.BuildResponse(request, result, "musicFolders"); + } + + [HttpGet("getPlaylist.view")] + [HttpPost("getPlaylist.view")] + [ProducesResponseType(200)] + public async Task GetPlaylist([FromQuery]Request request) + { + var result = await this.SubsonicService.GetPlaylist(request, null); + return this.BuildResponse(request, result, "playlist"); + } + + [HttpGet("getPlaylists.view")] + [HttpPost("getPlaylists.view")] + [ProducesResponseType(200)] + public async Task GetPlaylists([FromQuery]Request request, string username) + { + var result = await this.SubsonicService.GetPlaylists(request, null, username); + return this.BuildResponse(request, result, "playlists"); + } + + [HttpGet("getPodcasts.view")] + [HttpPost("getPodcasts.view")] + [ProducesResponseType(200)] + public async Task GetPodcasts([FromQuery]Request request, bool includeEpisodes) + { + var result = await this.SubsonicService.GetPodcasts(request); + return this.BuildResponse(request, result, "podcasts"); + } + + [HttpGet("getRandomSongs.view")] + [HttpPost("getRandomSongs.view")] + [ProducesResponseType(200)] + public async Task GetRandomSongs([FromQuery]Request request) + { + var result = await this.SubsonicService.GetRandomSongs(request, null); + return this.BuildResponse(request, result, "randomSongs"); + } + + [HttpGet("getStarred.view")] + [HttpPost("getStarred.view")] + [ProducesResponseType(200)] + public async Task GetStarred([FromQuery]Request request) + { + var result = await this.SubsonicService.GetStarred(request, null, StarredVersion.One); + return this.BuildResponse(request, result, "starred"); + } + + [HttpGet("getStarred2.view")] + [HttpPost("getStarred2.view")] + [ProducesResponseType(200)] + public async Task GetStarred2([FromQuery]Request request) + { + var result = await this.SubsonicService.GetStarred(request, null, StarredVersion.Two); + return this.BuildResponse(request, result, "starred"); + } + + [HttpGet("getUser.view")] + [HttpPost("getUser.view")] + [ProducesResponseType(200)] + public async Task GetUser([FromQuery]Request request, string username) + { + var result = await this.SubsonicService.GetUser(request, username); + return this.BuildResponse(request, result, "user"); + } + + [HttpGet("ping.view")] + [HttpPost("ping.view")] + [ProducesResponseType(200)] + public IActionResult Ping([FromQuery]Request request) + { + if (request.IsJSONRequest) + { + var result = this.SubsonicService.Ping(request); + return this.BuildResponse(request, result); + } + return Content("", "application/xml"); } /// @@ -239,16 +302,16 @@ namespace Roadie.Api.Controllers public async Task Search([FromQuery]Request request) { var result = await this.SubsonicService.Search(request, null, SearchVersion.One); - return this.BuildResponse(request, result.Data, "searchResult"); + return this.BuildResponse(request, result, "searchResult"); } [HttpGet("search2.view")] [HttpPost("search2.view")] - [ProducesResponseType(200)] + [ProducesResponseType(200)] public async Task Search2([FromQuery]Request request) { var result = await this.SubsonicService.Search(request, null, SearchVersion.Two); - return this.BuildResponse(request, result.Data, "searchResult2"); + return this.BuildResponse(request, result, "searchResult2"); } [HttpGet("search3.view")] @@ -257,19 +320,9 @@ namespace Roadie.Api.Controllers public async Task Search3([FromQuery]Request request) { var result = await this.SubsonicService.Search(request, null, SearchVersion.Three); - return this.BuildResponse(request, result.Data, "searchResult3"); + return this.BuildResponse(request, result, "searchResult3"); } - [HttpGet("getAlbum.view")] - [HttpPost("getAlbum.view")] - [ProducesResponseType(200)] - public async Task GetAlbum([FromQuery]Request request) - { - var result = await this.SubsonicService.GetAlbum(request, null); - return this.BuildResponse(request, result.Data, "album"); - } - - [HttpGet("stream.view")] [HttpPost("stream.view")] [ProducesResponseType(200)] @@ -287,21 +340,41 @@ namespace Roadie.Api.Controllers private string BuildJsonResult(Response response, string responseType) { + var status = response?.status.ToString(); + var version = response?.version ?? Roadie.Api.Services.SubsonicService.SubsonicVersion; if (responseType == null) { - return "{ \"subsonic-response\": { \"status\":\"" + response.status.ToString() + "\", \"version\": \"" + response.version + "\" }}"; + return "{ \"subsonic-response\": { \"status\":\"" + status + "\", \"version\": \"" + version + "\" }}"; } - return "{ \"subsonic-response\": { \"status\":\"" + response.status.ToString() + "\", \"version\": \"" + response.version + "\", \"" + responseType + "\":" + JsonConvert.SerializeObject(response.Item) + "}}"; + return "{ \"subsonic-response\": { \"status\":\"" + status + "\", \"version\": \"" + version + "\", \"" + responseType + "\":" + response != null ? JsonConvert.SerializeObject(response.Item) : string.Empty + "}}"; } - private IActionResult BuildResponse(Request request, Response response = null, string reponseType = null) + private IActionResult SendError(Request request, SubsonicOperationResult response = null, string responseType = null) { - var acceptHeader = this.Request.Headers["Accept"]; - this.Logger.LogTrace($"Subsonic Request: Method [{ this.Request.Method }], Accept Header [{ acceptHeader }], Path [{ this.Request.Path }], Query String [{ this.Request.QueryString }], Request [{ JsonConvert.SerializeObject(request) }] ResponseType [{ reponseType }]"); + var version = response?.Data?.version ?? Roadie.Api.Services.SubsonicService.SubsonicVersion; + string errorDescription = response?.ErrorCode?.DescriptionAttr(); + int? errorCode = (int?)response?.ErrorCode; if (request.IsJSONRequest) { this.Response.ContentType = "application/json"; - return Content(this.BuildJsonResult(response, reponseType)); + return Content("{ \"subsonic-response\": { \"status\":\"failed\", \"version\": \"" + version + "\", \"error\":{\"code\":\"" + errorCode + "\",\"message\":\"" + errorDescription + "\"}}}"); + } + this.Response.ContentType = "application/xml"; + return Content($""); + } + + private IActionResult BuildResponse(Request request, SubsonicOperationResult response = null, string responseType = null) + { + var acceptHeader = this.Request.Headers["Accept"]; + this.Logger.LogTrace($"Subsonic Request: Method [{ this.Request.Method }], Accept Header [{ acceptHeader }], Path [{ this.Request.Path }], Query String [{ this.Request.QueryString }], Response Error Code [{ response.ErrorCode }], Request [{ JsonConvert.SerializeObject(request) }] ResponseType [{ responseType }]"); + if (response.ErrorCode.HasValue) + { + return this.SendError(request, response, responseType); + } + if (request.IsJSONRequest) + { + this.Response.ContentType = "application/json"; + return Content(this.BuildJsonResult(response.Data, responseType)); } this.Response.ContentType = "application/xml"; return Ok(response); diff --git a/RoadieApi/Services/ArtistService.cs b/RoadieApi/Services/ArtistService.cs index 278ca93..50565cf 100644 --- a/RoadieApi/Services/ArtistService.cs +++ b/RoadieApi/Services/ArtistService.cs @@ -312,6 +312,7 @@ namespace Roadie.Api.Services } var result = (from a in this.DbContext.Artists + where (request.FilterToArtistId == null || a.RoadieId == request.FilterToArtistId) where (request.FilterMinimumRating == null || a.Rating >= request.FilterMinimumRating.Value) where (request.FilterValue == "" || (a.Name.Contains(request.FilterValue) || a.SortName.Contains(request.FilterValue) || a.AlternateNames.Contains(request.FilterValue))) where (!request.FilterFavoriteOnly || favoriteArtistIds.Contains(a.Id)) diff --git a/RoadieApi/Services/ISubsonicService.cs b/RoadieApi/Services/ISubsonicService.cs index f1858e5..365fe47 100644 --- a/RoadieApi/Services/ISubsonicService.cs +++ b/RoadieApi/Services/ISubsonicService.cs @@ -1,45 +1,53 @@ -using Roadie.Library; -using Roadie.Library.Models.ThirdPartyApi.Subsonic; +using Roadie.Library.Models.ThirdPartyApi.Subsonic; using System.Threading.Tasks; namespace Roadie.Api.Services { public interface ISubsonicService { - Task> GetAlbum(Request request, Roadie.Library.Models.Users.User roadieUser); + Task> GetAlbum(Request request, Roadie.Library.Models.Users.User roadieUser); - Task> GetAlbumList(Request request, Roadie.Library.Models.Users.User roadieUser, AlbumListVersions version); + Task> GetAlbumList(Request request, Roadie.Library.Models.Users.User roadieUser, AlbumListVersions version); - Task> GetArtistInfo(Request request, string id, int? count, bool includeNotPresent, ArtistInfoVersion version); + Task> GetArtistInfo(Request request, int? count, bool includeNotPresent, ArtistInfoVersion version); - Task> GetArtists(Request request, Roadie.Library.Models.Users.User roadieUser); + Task> GetArtists(Request request, Roadie.Library.Models.Users.User roadieUser); - Task> GetCoverArt(Request request, int? size); + Task> GetCoverArt(Request request, int? size); - Task> GetGenres(Request request); + Task> GetGenres(Request request); - Task> GetIndexes(Request request, Roadie.Library.Models.Users.User roadieUser, string musicFolderId = null, long? ifModifiedSince = null); + Task> GetIndexes(Request request, Roadie.Library.Models.Users.User roadieUser, long? ifModifiedSince = null); - OperationResult GetLicense(Request request); + SubsonicOperationResult GetLicense(Request request); - Task> GetMusicDirectory(Request request, Roadie.Library.Models.Users.User roadieUser, string id); + SubsonicOperationResult GetLyrics(Request request, string artistId, string title); - Task> GetMusicFolders(Request request); + Task> GetMusicDirectory(Request request, Roadie.Library.Models.Users.User roadieUser); - Task> GetPlaylist(Request request, Roadie.Library.Models.Users.User roadieUser, string id); + Task> GetMusicFolders(Request request); - Task> GetPlaylists(Request request, Roadie.Library.Models.Users.User roadieUser, string filterToUserName); + Task> GetPlaylist(Request request, Roadie.Library.Models.Users.User roadieUser); - Task> GetPodcasts(Request request); + Task> GetPlaylists(Request request, Roadie.Library.Models.Users.User roadieUser, string filterToUserName); - Task> GetRandomSongs(Request request, Roadie.Library.Models.Users.User roadieUser); + Task> GetPodcasts(Request request); - Task> GetStarred(Request request, Roadie.Library.Models.Users.User roadieUser, StarredVersion version); + Task> GetRandomSongs(Request request, Roadie.Library.Models.Users.User roadieUser); - Task> GetUser(Request request, string username); + Task> GetStarred(Request request, Roadie.Library.Models.Users.User roadieUser, StarredVersion version); - OperationResult Ping(Request request); + Task> GetUser(Request request, string username); - Task> Search(Request request, Roadie.Library.Models.Users.User roadieUser, SearchVersion version); + SubsonicOperationResult GetVideos(Request request); + + SubsonicOperationResult Ping(Request request); + + Task> Search(Request request, Roadie.Library.Models.Users.User roadieUser, SearchVersion version); + + Task> GetAlbumInfo(Request request, Roadie.Library.Models.Users.User roadieUser, AlbumInfoVersion version); + + Task> GetArtist(Request request, Roadie.Library.Models.Users.User roadieUser); + Task> GetSong(Request request, Roadie.Library.Models.Users.User roadieUser); } } \ No newline at end of file diff --git a/RoadieApi/Services/ServiceBase.cs b/RoadieApi/Services/ServiceBase.cs index 169f1c3..618f482 100644 --- a/RoadieApi/Services/ServiceBase.cs +++ b/RoadieApi/Services/ServiceBase.cs @@ -356,5 +356,10 @@ namespace Roadie.Api.Services return new Image($"{this.HttpContext.ImageBaseUrl }/{type}/{id}"); } + protected string MakeLastFmUrl(string artistName, string releaseTitle) + { + return "http://www.last.fm/music/" + this.HttpEncoder.UrlEncode($"{ artistName }/{ releaseTitle }"); + } + } } \ No newline at end of file diff --git a/RoadieApi/Services/SubsonicService.cs b/RoadieApi/Services/SubsonicService.cs index ec6eb89..0a0b036 100644 --- a/RoadieApi/Services/SubsonicService.cs +++ b/RoadieApi/Services/SubsonicService.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Identity; +using Mapster; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Roadie.Library; @@ -64,22 +65,22 @@ namespace Roadie.Api.Services this.PlaylistService = playlistService; } - public async Task> GetAlbum(subsonic.Request request, User roadieUser) + public async Task> GetAlbum(subsonic.Request request, User roadieUser) { if (!request.ReleaseId.HasValue) { - return new OperationResult(true, $"Invalid Release [{ request.ReleaseId}]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.ReleaseId}]"); } var release = this.GetRelease(request.ReleaseId.Value); if (release == null) { - return new OperationResult(true, $"Invalid Release [{ request.ReleaseId}]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.ReleaseId}]"); } var pagedRequest = request.PagedRequest; var releaseTracks = await this.TrackService.List(roadieUser, pagedRequest, false, request.ReleaseId); var userRelease = roadieUser == null ? null : this.DbContext.UserReleases.FirstOrDefault(x => x.ReleaseId == release.Id && x.UserId == roadieUser.Id); var genre = release.Genres.FirstOrDefault(); - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -113,7 +114,7 @@ namespace Roadie.Api.Services /// /// Returns a list of random, newest, highest rated etc. albums. Similar to the album lists on the home page of the Subsonic web interface. /// - public async Task> GetAlbumList(subsonic.Request request, User roadieUser, subsonic.AlbumListVersions version) + public async Task> GetAlbumList(subsonic.Request request, User roadieUser, subsonic.AlbumListVersions version) { var releaseResult = new Library.Models.Pagination.PagedResult(); @@ -136,18 +137,18 @@ namespace Roadie.Api.Services break; default: - return new OperationResult($"Unknown Album List Type [{ request.Type}]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown Album List Type [{ request.Type}]"); } if (!releaseResult.IsSuccess) { - return new OperationResult(releaseResult.Message); + return new subsonic.SubsonicOperationResult(releaseResult.Message); } switch (version) { case subsonic.AlbumListVersions.One: - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -163,7 +164,7 @@ namespace Roadie.Api.Services }; case subsonic.AlbumListVersions.Two: - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -179,30 +180,29 @@ namespace Roadie.Api.Services }; default: - return new OperationResult($"Unknown AlbumListVersions [{ version }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown AlbumListVersions [{ version }]"); } } /// /// Returns artist info with biography, image URLs and similar artists, using data from last.fm. /// - public async Task> GetArtistInfo(subsonic.Request request, string id, int? count, bool includeNotPresent, subsonic.ArtistInfoVersion version) + public async Task> GetArtistInfo(subsonic.Request request, int? count, bool includeNotPresent, subsonic.ArtistInfoVersion version) { - var artistId = SafeParser.ToGuid(id); - if (!artistId.HasValue) + if (!request.ArtistId.HasValue) { - return new OperationResult(true, $"Invalid ArtistId [{ id }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid ArtistId [{ request.id }]"); } - var artist = this.GetArtist(artistId.Value); + var artist = this.GetArtist(request.ArtistId.Value); if (artist == null) { - return new OperationResult(true, $"Invalid ArtistId [{ id }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid ArtistId [{ request.id }]"); } switch (version) { case subsonic.ArtistInfoVersion.One: - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -215,7 +215,7 @@ namespace Roadie.Api.Services }; case subsonic.ArtistInfoVersion.Two: - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -228,11 +228,11 @@ namespace Roadie.Api.Services }; default: - return new OperationResult($"Unknown ArtistInfoVersion [{ version }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown ArtistInfoVersion [{ version }]"); } } - public async Task> GetArtists(subsonic.Request request, User roadieUser) + public async Task> GetArtists(subsonic.Request request, User roadieUser) { var indexes = new List(); // Indexes for Artists alphabetically @@ -249,7 +249,7 @@ namespace Roadie.Api.Services artist = this.SubsonicArtistID3sForArtists(artistGroup) }); }; - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -268,10 +268,10 @@ namespace Roadie.Api.Services /// /// Returns a cover art image. /// - public async Task> GetCoverArt(subsonic.Request request, int? size) + public async Task> GetCoverArt(subsonic.Request request, int? size) { var sw = Stopwatch.StartNew(); - var result = new FileOperationResult + var result = new subsonic.SubsonicFileOperationResult { Data = new Roadie.Library.Models.Image() }; @@ -281,7 +281,7 @@ namespace Roadie.Api.Services var artistImage = await this.ImageService.ArtistImage(request.ArtistId.Value, size, size); if (!artistImage.IsSuccess) { - return artistImage; + return artistImage.Adapt>(); } result.Data.Bytes = artistImage.Data.Bytes; } @@ -290,7 +290,7 @@ namespace Roadie.Api.Services var trackimage = await this.ImageService.TrackImage(request.TrackId.Value, size, size); if (!trackimage.IsSuccess) { - return trackimage; + return trackimage.Adapt>(); } result.Data.Bytes = trackimage.Data.Bytes; } @@ -299,7 +299,7 @@ namespace Roadie.Api.Services var collection = this.GetCollection(request.CollectionId.Value); if (collection == null) { - return new FileOperationResult(true, $"Invalid CollectionId [{ request.CollectionId}]"); + return new subsonic.SubsonicFileOperationResult(true, $"Invalid CollectionId [{ request.CollectionId}]"); } result.Data.Bytes = collection.Thumbnail; } @@ -308,7 +308,7 @@ namespace Roadie.Api.Services var release = this.GetRelease(request.ReleaseId.Value); if (release == null) { - return new FileOperationResult(true, $"Invalid ReleaseId [{ request.ReleaseId}]"); + return new subsonic.SubsonicFileOperationResult(true, $"Invalid ReleaseId [{ request.ReleaseId}]"); } result.Data.Bytes = release.Thumbnail; } @@ -317,7 +317,7 @@ namespace Roadie.Api.Services var playlist = this.GetPlaylist(request.PlaylistId.Value); if (playlist == null) { - return new FileOperationResult(true, $"Invalid PlaylistId [{ request.PlaylistId}]"); + return new subsonic.SubsonicFileOperationResult(true, $"Invalid PlaylistId [{ request.PlaylistId}]"); } result.Data.Bytes = playlist.Thumbnail; } @@ -326,7 +326,7 @@ namespace Roadie.Api.Services var user = this.GetUser(request.u); if (user == null) { - return new FileOperationResult(true, $"Invalid Username [{ request.u}]"); + return new subsonic.SubsonicFileOperationResult(true, $"Invalid Username [{ request.u}]"); } result.Data.Bytes = user.Avatar; } @@ -339,7 +339,7 @@ namespace Roadie.Api.Services } result.IsSuccess = result.Data.Bytes != null; sw.Stop(); - return new FileOperationResult(result.Messages) + return new subsonic.SubsonicFileOperationResult(result.Messages) { Data = result.Data, ETag = result.ETag, @@ -354,7 +354,7 @@ namespace Roadie.Api.Services /// /// Returns all genres /// - public async Task> GetGenres(subsonic.Request request) + public async Task> GetGenres(subsonic.Request request) { var genres = (from g in this.DbContext.Genres let albumCount = (from rg in this.DbContext.ReleaseGenres @@ -372,7 +372,7 @@ namespace Roadie.Api.Services value = g.Name }).OrderBy(x => x.value).ToArray(); - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -394,10 +394,10 @@ namespace Roadie.Api.Services /// Query from application. /// If specified, only return artists in the music folder with the given ID. /// If specified, only return a result if the artist collection has changed since the given time (in milliseconds since 1 Jan 1970). - public async Task> GetIndexes(subsonic.Request request, User roadieUser, string musicFolderId = null, long? ifModifiedSince = null) + public async Task> GetIndexes(subsonic.Request request, User roadieUser, long? ifModifiedSince = null) { var modifiedSinceFilter = ifModifiedSince.HasValue ? (DateTime?)ifModifiedSince.Value.FromUnixTime() : null; - subsonic.MusicFolder musicFolderFilter = string.IsNullOrEmpty(musicFolderId) ? new subsonic.MusicFolder() : this.MusicFolders().FirstOrDefault(x => x.id == SafeParser.ToNumber(musicFolderId)); + subsonic.MusicFolder musicFolderFilter = !request.MusicFolderId.HasValue ? new subsonic.MusicFolder() : this.MusicFolders().FirstOrDefault(x => x.id == request.MusicFolderId.Value); var indexes = new List(); if (musicFolderFilter.id == this.CollectionMusicFolder().id) @@ -443,7 +443,7 @@ namespace Roadie.Api.Services }); }; } - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -462,9 +462,9 @@ namespace Roadie.Api.Services /// /// Get details about the software license. Takes no extra parameters. Roadies gives everyone a premium 1 year license everytime they ask :) /// - public OperationResult GetLicense(subsonic.Request request) + public subsonic.SubsonicOperationResult GetLicense(subsonic.Request request) { - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -489,7 +489,7 @@ namespace Roadie.Api.Services /// Query from application. /// A string which uniquely identifies the music folder. Obtained by calls to getIndexes or getMusicDirectory. /// - public async Task> GetMusicDirectory(subsonic.Request request, User roadieUser, string id) + public async Task> GetMusicDirectory(subsonic.Request request, User roadieUser) { var directory = new subsonic.Directory(); var user = this.GetUser(roadieUser?.UserId); @@ -500,7 +500,7 @@ namespace Roadie.Api.Services var artist = this.GetArtist(request.ArtistId.Value); if (artist == null) { - return new OperationResult(true, $"Invalid ArtistId [{ request.ArtistId}]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid ArtistId [{ request.ArtistId}]"); } directory.id = subsonic.Request.ArtistIdIdentifier + artist.RoadieId.ToString(); directory.name = artist.Name; @@ -521,7 +521,7 @@ namespace Roadie.Api.Services var collection = this.GetCollection(request.CollectionId.Value); if (collection == null) { - return new OperationResult(true, $"Invalid CollectionId [{ request.CollectionId}]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid CollectionId [{ request.CollectionId}]"); } directory.id = subsonic.Request.CollectionIdentifier + collection.RoadieId.ToString(); directory.name = collection.Name; @@ -536,7 +536,7 @@ namespace Roadie.Api.Services var release = this.GetRelease(request.ReleaseId.Value); if (release == null) { - return new OperationResult(true, $"Invalid ReleaseId [{ request.ReleaseId}]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid ReleaseId [{ request.ReleaseId}]"); } directory.id = subsonic.Request.ReleaseIdIdentifier + release.RoadieId.ToString(); directory.name = release.Title; @@ -555,9 +555,9 @@ namespace Roadie.Api.Services } else { - return new OperationResult($"Unknown GetMusicDirectory Type [{ JsonConvert.SerializeObject(request) }], id [{ id }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown GetMusicDirectory Type [{ JsonConvert.SerializeObject(request) }], id [{ request.id }]"); } - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -573,9 +573,9 @@ namespace Roadie.Api.Services /// /// Returns all configured top-level music folders. Takes no extra parameters. /// - public async Task> GetMusicFolders(subsonic.Request request) + public async Task> GetMusicFolders(subsonic.Request request) { - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -594,23 +594,22 @@ namespace Roadie.Api.Services /// /// Returns a listing of files in a saved playlist. /// - public async Task> GetPlaylist(subsonic.Request request, User roadieUser, string id) + public async Task> GetPlaylist(subsonic.Request request, User roadieUser) { - var playListId = SafeParser.ToGuid(id); - if (!playListId.HasValue) + if (!request.PlaylistId.HasValue) { - return new OperationResult(true, $"Invalid PlaylistId [{ id }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid PlaylistId [{ request.id }]"); } var pagedRequest = request.PagedRequest; - pagedRequest.FilterToPlaylistId = playListId; + pagedRequest.FilterToPlaylistId = request.PlaylistId.Value; var playlistResult = await this.PlaylistService.List(pagedRequest, roadieUser); var playlist = playlistResult.Rows.Any() ? playlistResult.Rows.First() : null; if (playlist == null) { - return new OperationResult(true, $"Invalid PlaylistId [{ id }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid PlaylistId [{ request.id }]"); } var tracksForPlaylist = await this.TrackService.List(roadieUser, pagedRequest); - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -626,7 +625,7 @@ namespace Roadie.Api.Services /// /// Returns all playlists a user is allowed to play. /// - public async Task> GetPlaylists(subsonic.Request request, User roadieUser, string filterToUserName) + public async Task> GetPlaylists(subsonic.Request request, User roadieUser, string filterToUserName) { var playlists = (from playlist in this.DbContext.Playlists join u in this.DbContext.Users on playlist.UserId equals u.Id @@ -654,7 +653,7 @@ namespace Roadie.Api.Services } ); - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -673,9 +672,9 @@ namespace Roadie.Api.Services /// /// Returns all Podcast channels the server subscribes to, and (optionally) their episodes. This method can also be used to return details for only one channel - refer to the id parameter. A typical use case for this method would be to first retrieve all channels without episodes, and then retrieve all episodes for the single channel the user selects. /// - public async Task> GetPodcasts(subsonic.Request request) + public async Task> GetPodcasts(subsonic.Request request) { - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -691,13 +690,13 @@ namespace Roadie.Api.Services /// /// Returns random songs matching the given criteria. /// - public async Task> GetRandomSongs(subsonic.Request request, User roadieUser) + public async Task> GetRandomSongs(subsonic.Request request, User roadieUser) { var songs = new List(); var randomSongs = await this.TrackService.List(roadieUser, request.PagedRequest, true); - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -716,7 +715,7 @@ namespace Roadie.Api.Services /// /// Returns starred songs, albums and artists. /// - public async Task> GetStarred(subsonic.Request request, User roadieUser, subsonic.StarredVersion version) + public async Task> GetStarred(subsonic.Request request, User roadieUser, subsonic.StarredVersion version) { var pagedRequest = request.PagedRequest; pagedRequest.FilterFavoriteOnly = true; @@ -728,7 +727,7 @@ namespace Roadie.Api.Services switch (version) { case subsonic.StarredVersion.One: - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -746,7 +745,7 @@ namespace Roadie.Api.Services }; case subsonic.StarredVersion.Two: - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -764,21 +763,21 @@ namespace Roadie.Api.Services }; default: - return new OperationResult($"Unknown StarredVersion [{ version }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown StarredVersion [{ version }]"); } } /// /// Get details about a given user, including which authorization roles and folder access it has. Can be used to enable/disable certain features in the client, such as jukebox control. /// - public async Task> GetUser(subsonic.Request request, string username) + public async Task> GetUser(subsonic.Request request, string username) { var user = this.GetUser(username); if (user == null) { - return new OperationResult(true, $"Invalid Username [{ username }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Username [{ username }]"); } - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -794,9 +793,9 @@ namespace Roadie.Api.Services /// /// Used to test connectivity with the server. Takes no extra parameters. /// - public OperationResult Ping(subsonic.Request request) + public subsonic.SubsonicOperationResult Ping(subsonic.Request request) { - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -810,7 +809,7 @@ namespace Roadie.Api.Services /// /// Returns albums, artists and songs matching the given search criteria. Supports paging through the result. /// - public async Task> Search(subsonic.Request request, User roadieUser, subsonic.SearchVersion version) + public async Task> Search(subsonic.Request request, User roadieUser, subsonic.SearchVersion version) { var query = this.HttpEncoder.UrlDecode(request.Query).Replace("*", "").Replace("%", "").Replace(";", ""); @@ -842,10 +841,10 @@ namespace Roadie.Api.Services switch (version) { case subsonic.SearchVersion.One: - return new OperationResult("Deprecated since 1.4.0, use search2 instead."); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.IncompatibleClientRestProtocolVersion,"Deprecated since 1.4.0, use search2 instead."); case subsonic.SearchVersion.Two: - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -863,7 +862,7 @@ namespace Roadie.Api.Services }; case subsonic.SearchVersion.Three: - return new OperationResult + return new subsonic.SubsonicOperationResult { IsSuccess = true, Data = new subsonic.Response @@ -881,11 +880,287 @@ namespace Roadie.Api.Services }; default: - return new OperationResult($"Unknown SearchVersion [{ version }]"); + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown SearchVersion [{ version }]"); } } - public subsonic.ArtistInfo2 SubsonicArtistInfo2InfoForArtist(data.Artist artist) + /// + /// Returns album notes, image URLs etc, using data from last.fm. + /// + public async Task> GetAlbumInfo(subsonic.Request request, User roadieUser, subsonic.AlbumInfoVersion version) + { + if (!request.ReleaseId.HasValue) + { + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]"); + } + var release = this.GetRelease(request.ReleaseId.Value); + if (release == null) + { + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]"); + } + switch (version) + { + case subsonic.AlbumInfoVersion.One: + case subsonic.AlbumInfoVersion.Two: + return new subsonic.SubsonicOperationResult + { + IsSuccess = true, + Data = new subsonic.Response + { + version = SubsonicService.SubsonicVersion, + status = subsonic.ResponseStatus.ok, + ItemElementName = subsonic.ItemChoiceType.albumInfo, + Item = new subsonic.AlbumInfo + { + largeImageUrl = this.MakeImage(release.RoadieId, "release", this.Configuration.LargeImageSize).Url, + mediumImageUrl = this.MakeImage(release.RoadieId, "release", this.Configuration.MediumImageSize).Url, + smallImageUrl = this.MakeImage(release.RoadieId, "release", this.Configuration.SmallImageSize).Url, + lastFmUrl = this.MakeLastFmUrl(release.Artist.Name, release.Title), + musicBrainzId = release.MusicBrainzId, + notes = release.Profile + } + } + }; + + default: + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown Album Info Version [{ request.Type}]"); + + } + } + + /// + /// Returns details for an artist, including a list of albums. This method organizes music according to ID3 tags. + /// + public async Task> GetArtist(subsonic.Request request, User roadieUser) + { + if (!request.ArtistId.HasValue) + { + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]"); + } + var pagedRequest = request.PagedRequest; + pagedRequest.FilterToArtistId = request.ArtistId.Value; + var artistResult = await this.ArtistService.List(roadieUser, pagedRequest); + var artist = artistResult.Rows.Any() ? artistResult.Rows.First() : null; + if (artist == null) + { + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]"); + } + return new subsonic.SubsonicOperationResult + { + IsSuccess = true, + Data = new subsonic.Response + { + version = SubsonicService.SubsonicVersion, + status = subsonic.ResponseStatus.ok, + ItemElementName = subsonic.ItemChoiceType.artist, + Item = this.SubsonicArtistForArtist(artist) + } + }; + } + + /// + /// Returns details for a song. + /// + public async Task> GetSong(subsonic.Request request, User roadieUser) + { + if (!request.TrackId.HasValue) + { + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track [{ request.id }]"); + } + var pagedRequest = request.PagedRequest; + pagedRequest.FilterToArtistId = request.TrackId.Value; + var trackResult = await this.TrackService.List(roadieUser, pagedRequest); + var track = trackResult.Rows.Any() ? trackResult.Rows.First() : null; + if (track == null) + { + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track [{ request.id }]"); + } + return new subsonic.SubsonicOperationResult + { + IsSuccess = true, + Data = new subsonic.Response + { + version = SubsonicService.SubsonicVersion, + status = subsonic.ResponseStatus.ok, + ItemElementName = subsonic.ItemChoiceType.song, + Item = this.SubsonicChildForTrack(track) + } + }; + } + + /// + /// Returns top songs for the given artist, using data from last.fm. + /// + public async Task> GetTopSongs(subsonic.Request request, User roadieUser, string artistId, int? count = 50) + { + // TODO + throw new NotImplementedException(); + } + + /// + /// Returns songs in a given genre. + /// + public async Task> GetSongsByGenre(subsonic.Request request, User roadieUser, string genre, int? count = 10, int? offset = 0) + { + // TODO + throw new NotImplementedException(); + } + + /// + /// Returns all video files. + /// + public subsonic.SubsonicOperationResult GetVideos(subsonic.Request request) + { + return new subsonic.SubsonicOperationResult + { + IsSuccess = true, + Data = new subsonic.Response + { + version = SubsonicService.SubsonicVersion, + status = subsonic.ResponseStatus.ok, + ItemElementName = subsonic.ItemChoiceType.videos, + Item = new subsonic.Videos + { + video = new subsonic.Child[0] + } + } + }; + } + + /// + /// Searches for and returns lyrics for a given song + /// + public subsonic.SubsonicOperationResult GetLyrics(subsonic.Request request, string artistId, string title) + { + return new subsonic.SubsonicOperationResult + { + IsSuccess = true, + Data = new subsonic.Response + { + version = SubsonicService.SubsonicVersion, + status = subsonic.ResponseStatus.ok, + ItemElementName = subsonic.ItemChoiceType.lyrics, + Item = new subsonic.Lyrics + { + artist = artistId, + title = title, + Text = new string[0] + } + } + }; + } + + /// + /// Returns a random collection of songs from the given artist and similar artists, using data from last.fm. Typically used for artist radio features. + /// + public async Task> GetSimliarSongs(subsonic.Request request, User roadieUser, subsonic.SimilarSongsVersion version, int? count = 50) + { + // TODO + throw new NotImplementedException(); + } + + /// + /// Downloads a given media file. Similar to stream, but this method returns the original media data without transcoding or downsampling. + /// + public async Task> Download(subsonic.Request request, User roadieUser) + { + // TODO + throw new NotImplementedException(); + } + + /// + /// Authenticate the given credentials and return the corresponding ApplicationUser + /// + public async Task> Authenticate(subsonic.Request request, string username, string password) + { + // TODO + + //public user CheckPasswordGetUser(ICacheManager cacheManager, RoadieDbContext context) + //{ + // user user = null; + // if (string.IsNullOrEmpty(this.UsernameValue)) + // { + // return null; + // } + // try + // { + // var cacheKey = string.Format("urn:user:byusername:{0}", this.UsernameValue.ToLower()); + // var resultInCache = cacheManager.Get(cacheKey); + // if (resultInCache == null) + // { + // user = context.users.FirstOrDefault(x => x.username.Equals(this.UsernameValue, StringComparison.OrdinalIgnoreCase)); + // var claims = new List + // { + // new Claim(Library.Authentication.ClaimTypes.UserId, user.id.ToString()).ToString() + // }; + // var sql = @"select ur.name FROM `userrole` ur LEFT JOIN usersInRoles uir on ur.id = uir.userRoleId where uir.userId = " + user.id + ";"; + // var userRoles = context.Database.SqlQuery(sql).ToList(); + // if (userRoles != null && userRoles.Any()) + // { + // foreach (var userRole in userRoles) + // { + // claims.Add(new Claim(Library.Authentication.ClaimTypes.UserRole, userRole).ToString()); + // } + // } + // user.ClaimsValue = claims; + // cacheManager.Add(cacheKey, user); + // } + // else + // { + // user = resultInCache; + // } + // if (user == null) + // { + // return null; + // } + // var password = this.Password; + // var wasAuthenticatedAgainstPassword = false; + // if (!string.IsNullOrEmpty(this.s)) + // { + // var token = ModuleBase.MD5Hash((user.apiToken ?? user.email) + this.s); + // if (!token.Equals(this.t, StringComparison.OrdinalIgnoreCase)) + // { + // user = null; + // } + // else + // { + // wasAuthenticatedAgainstPassword = true; + // } + // } + // else + // { + // if (user != null && !BCrypt.Net.BCrypt.Verify(password, user.password)) + // { + // user = null; + // } + // else + // { + // wasAuthenticatedAgainstPassword = true; + // } + // } + // if (wasAuthenticatedAgainstPassword) + // { + // // Since API dont update LastLogin which likely invalidates any browser logins + // user.lastApiAccess = DateTime.UtcNow; + // context.SaveChanges(); + // } + // return user; + // } + // catch (Exception ex) + // { + // Trace.WriteLine("Error CheckPassword [" + ex.Serialize() + "]"); + // } + // return null; + //} + + + + throw new NotImplementedException(); + } + + #region Privates + + private subsonic.ArtistInfo2 SubsonicArtistInfo2InfoForArtist(data.Artist artist) { return new subsonic.ArtistInfo2 { @@ -898,7 +1173,7 @@ namespace Roadie.Api.Services }; } - public subsonic.ArtistInfo SubsonicArtistInfoForArtist(data.Artist artist) + private subsonic.ArtistInfo SubsonicArtistInfoForArtist(data.Artist artist) { return new subsonic.ArtistInfo { @@ -909,7 +1184,7 @@ namespace Roadie.Api.Services similarArtist = new subsonic.Artist[0], smallImageUrl = this.MakeImage(artist.RoadieId, "artist", this.Configuration.SmallImageSize).Url }; - } + } private string[] AllowedUsers() { @@ -1080,7 +1355,7 @@ namespace Roadie.Api.Services userRating = t.UserRating != null ? t.UserRating.Rating ?? 0 : 0, userRatingSpecified = t.UserRating != null, year = t.Year ?? 0, - yearSpecified = t.Year.HasValue, + yearSpecified = t.Year.HasValue, transcodedContentType = "audio/mpeg", transcodedSuffix = "mp3", isVideo = false, @@ -1159,5 +1434,7 @@ namespace Roadie.Api.Services folder = this.MusicFolders().Select(x => x.id).ToArray() }; } + + #endregion Privates } } \ No newline at end of file diff --git a/RoadieApi/Services/TrackService.cs b/RoadieApi/Services/TrackService.cs index 4bb8e05..6de05da 100644 --- a/RoadieApi/Services/TrackService.cs +++ b/RoadieApi/Services/TrackService.cs @@ -250,6 +250,7 @@ namespace Roadie.Api.Services from releaseArtist in aa.DefaultIfEmpty() where (t.Hash != null) where (releaseId == null || (releaseId != null && r.RoadieId == releaseId)) + where (request.FilterToTrackId == null || request.FilterToTrackId != null && t.RoadieId == request.FilterToTrackId) where (request.FilterToArtistId == null || request.FilterToArtistId != null && r.Artist.RoadieId == request.FilterToArtistId) where (request.FilterMinimumRating == null || t.Rating >= request.FilterMinimumRating.Value) where (request.FilterValue == "" || (t.Title.Contains(request.FilterValue) || t.AlternateNames.Contains(request.FilterValue))) diff --git a/RoadieLibrary/Extensions/GenericExt.cs b/RoadieLibrary/Extensions/GenericExt.cs index 41431d2..3dec47b 100644 --- a/RoadieLibrary/Extensions/GenericExt.cs +++ b/RoadieLibrary/Extensions/GenericExt.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; @@ -53,5 +54,12 @@ namespace Roadie.Library.Extensions } } + public static string DescriptionAttr(this T source) + { + FieldInfo fi = source.GetType().GetField(source.ToString()); + DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); + if (attributes != null && attributes.Length > 0) return attributes[0].Description; + else return source.ToString(); + } } } \ No newline at end of file diff --git a/RoadieLibrary/Extensions/StringExt.cs b/RoadieLibrary/Extensions/StringExt.cs index ea52df5..c518c7a 100644 --- a/RoadieLibrary/Extensions/StringExt.cs +++ b/RoadieLibrary/Extensions/StringExt.cs @@ -324,5 +324,6 @@ namespace Roadie.Library.Extensions } return input; } + } } \ No newline at end of file diff --git a/RoadieLibrary/Models/Pagination/PagedRequest.cs b/RoadieLibrary/Models/Pagination/PagedRequest.cs index 9fd4b1d..c5e2db5 100644 --- a/RoadieLibrary/Models/Pagination/PagedRequest.cs +++ b/RoadieLibrary/Models/Pagination/PagedRequest.cs @@ -99,6 +99,7 @@ namespace Roadie.Library.Models.Pagination public bool? FilterOnlyMissing { get; set; } public Guid? FilterToArtistId { get; set; } + public Guid? FilterToTrackId { get; set; } public Guid? FilterToCollectionId { get; set; } public Guid? FilterToPlaylistId { get; set; } diff --git a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/AlbumInfoVersion.cs b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/AlbumInfoVersion.cs new file mode 100644 index 0000000..98175eb --- /dev/null +++ b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/AlbumInfoVersion.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Roadie.Library.Models.ThirdPartyApi.Subsonic +{ + public enum AlbumInfoVersion + { + One, + Two + } +} diff --git a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/ErrorCodes.cs b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/ErrorCodes.cs new file mode 100644 index 0000000..5a8b5e8 --- /dev/null +++ b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/ErrorCodes.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; + +namespace Roadie.Library.Models.ThirdPartyApi.Subsonic +{ + public enum ErrorCodes + { + [Description("A generic error.")] + Generic = 0, + [Description("Required parameter is missing.")] + RequiredParameterMissing = 10, + [Description("Incompatible Subsonic REST protocol version. Client must upgrade.")] + IncompatibleClientRestProtocolVersion = 20, + [Description("Incompatible Subsonic REST protocol version. Server must upgrade.")] + IncompatibleServerRestProtocolVersion = 30, + [Description("Wrong username or password.")] + WrongUsernameOrPassword = 40, + [Description("Token authentication not supported for LDAP users.")] + TokenAuthenticatinNotSupportedForLDAP = 41, + [Description("User is not authorized for the given operation.")] + UserIsNotAuthorizedForGivenOperation = 50, + [Description("The trial period for the Subsonic server is over. Please upgrade to Subsonic Premium. Visit subsonic.org for details.")] + TrialPeriodSubsonicServerHasExpired = 60, + [Description("The requested data was not found")] + TheRequestedDataWasNotFound = 70 + } +} diff --git a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs index 7f23f4e..aec6d58 100644 --- a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs +++ b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs @@ -24,7 +24,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic } if (this.id.StartsWith(Request.ArtistIdIdentifier)) { - return SafeParser.ToGuid(this.id.Replace(Request.ArtistIdIdentifier, "")); + return SafeParser.ToGuid(this.id); } return null; } @@ -50,7 +50,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic } if (this.id.StartsWith(Request.CollectionIdentifier)) { - return SafeParser.ToGuid(this.id.Replace(Request.CollectionIdentifier, "")); + return SafeParser.ToGuid(this.id); } return null; } @@ -121,7 +121,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic } if (this.id.StartsWith(Request.PlaylistdIdentifier)) { - return SafeParser.ToGuid(this.id.Replace(Request.PlaylistdIdentifier, "")); + return SafeParser.ToGuid(this.id); } return null; } @@ -142,7 +142,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic } if (this.id.StartsWith(Request.ReleaseIdIdentifier)) { - return SafeParser.ToGuid(this.id.Replace(Request.ReleaseIdIdentifier, "")); + return SafeParser.ToGuid(this.id); } return null; } @@ -168,7 +168,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic } if (this.id.StartsWith(Request.TrackIdIdentifier)) { - return SafeParser.ToGuid(this.id.Replace(Request.TrackIdIdentifier, "")); + return SafeParser.ToGuid(this.id); } return null; } @@ -315,82 +315,6 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic #endregion Paging and List Related - //public user CheckPasswordGetUser(ICacheManager cacheManager, RoadieDbContext context) - //{ - // user user = null; - // if (string.IsNullOrEmpty(this.UsernameValue)) - // { - // return null; - // } - // try - // { - // var cacheKey = string.Format("urn:user:byusername:{0}", this.UsernameValue.ToLower()); - // var resultInCache = cacheManager.Get(cacheKey); - // if (resultInCache == null) - // { - // user = context.users.FirstOrDefault(x => x.username.Equals(this.UsernameValue, StringComparison.OrdinalIgnoreCase)); - // var claims = new List - // { - // new Claim(Library.Authentication.ClaimTypes.UserId, user.id.ToString()).ToString() - // }; - // var sql = @"select ur.name FROM `userrole` ur LEFT JOIN usersInRoles uir on ur.id = uir.userRoleId where uir.userId = " + user.id + ";"; - // var userRoles = context.Database.SqlQuery(sql).ToList(); - // if (userRoles != null && userRoles.Any()) - // { - // foreach (var userRole in userRoles) - // { - // claims.Add(new Claim(Library.Authentication.ClaimTypes.UserRole, userRole).ToString()); - // } - // } - // user.ClaimsValue = claims; - // cacheManager.Add(cacheKey, user); - // } - // else - // { - // user = resultInCache; - // } - // if (user == null) - // { - // return null; - // } - // var password = this.Password; - // var wasAuthenticatedAgainstPassword = false; - // if (!string.IsNullOrEmpty(this.s)) - // { - // var token = ModuleBase.MD5Hash((user.apiToken ?? user.email) + this.s); - // if (!token.Equals(this.t, StringComparison.OrdinalIgnoreCase)) - // { - // user = null; - // } - // else - // { - // wasAuthenticatedAgainstPassword = true; - // } - // } - // else - // { - // if (user != null && !BCrypt.Net.BCrypt.Verify(password, user.password)) - // { - // user = null; - // } - // else - // { - // wasAuthenticatedAgainstPassword = true; - // } - // } - // if (wasAuthenticatedAgainstPassword) - // { - // // Since API dont update LastLogin which likely invalidates any browser logins - // user.lastApiAccess = DateTime.UtcNow; - // context.SaveChanges(); - // } - // return user; - // } - // catch (Exception ex) - // { - // Trace.WriteLine("Error CheckPassword [" + ex.Serialize() + "]"); - // } - // return null; - //} + } } \ No newline at end of file diff --git a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/SimilarSongsVersion.cs b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/SimilarSongsVersion.cs new file mode 100644 index 0000000..5e8bd45 --- /dev/null +++ b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/SimilarSongsVersion.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Roadie.Library.Models.ThirdPartyApi.Subsonic +{ + public enum SimilarSongsVersion + { + One, + Two + } +} diff --git a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/SubsonicFileOperationResult.cs b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/SubsonicFileOperationResult.cs new file mode 100644 index 0000000..0b4ba7e --- /dev/null +++ b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/SubsonicFileOperationResult.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Roadie.Library.Models.ThirdPartyApi.Subsonic +{ + public class SubsonicFileOperationResult : FileOperationResult + { + public ErrorCodes ErrorCode { get; set; } + + public SubsonicFileOperationResult() + { + } + + public SubsonicFileOperationResult(string message) + : base(message) + { + } + + public SubsonicFileOperationResult(bool isNotFoundResult, string message) + : base(isNotFoundResult, message) + { + } + + public SubsonicFileOperationResult(IEnumerable messages = null) + : base(messages) + { + } + + public SubsonicFileOperationResult(bool isNotFoundResult, IEnumerable messages = null) + : base(isNotFoundResult, messages) + { + } + + } +} diff --git a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/SubsonicOperationResult.cs b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/SubsonicOperationResult.cs new file mode 100644 index 0000000..b517e4b --- /dev/null +++ b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/SubsonicOperationResult.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; + +namespace Roadie.Library.Models.ThirdPartyApi.Subsonic +{ + [Serializable] + public class SubsonicOperationResult : OperationResult + { + public ErrorCodes? ErrorCode { get; set; } + + public SubsonicOperationResult(bool isNotFoundResult, IEnumerable messages = null) + : base(isNotFoundResult, messages) + { + } + + public SubsonicOperationResult() + { + } + + public SubsonicOperationResult(IEnumerable messages = null) + : base(messages) + { + } + + public SubsonicOperationResult(string message = null) + : base(message) + { + } + + public SubsonicOperationResult(ErrorCodes error, string message = null) + : base(message) + { + this.ErrorCode = error; + } + + public SubsonicOperationResult(Exception error = null) + : base(error) + { + } + + public SubsonicOperationResult(string message = null, Exception error = null) + : base(message, error) + { + } + + } +} \ No newline at end of file diff --git a/RoadieLibrary/Utility/SafeParser.cs b/RoadieLibrary/Utility/SafeParser.cs index 87d07ee..c99685b 100644 --- a/RoadieLibrary/Utility/SafeParser.cs +++ b/RoadieLibrary/Utility/SafeParser.cs @@ -65,7 +65,12 @@ namespace Roadie.Library.Utility { return null; } - if (!Guid.TryParse(input.ToString(), out Guid result)) + var i = input.ToString(); + if(i[1] == ':') + { + i = i.Substring(2, i.Length - 2); + } + if (!Guid.TryParse(i.ToString(), out Guid result)) { return null; }