Subsonic API work

This commit is contained in:
Steven Hildreth 2018-11-22 07:48:32 -06:00
parent 27a2dfea93
commit 40c812fdd4
17 changed files with 773 additions and 321 deletions

View file

@ -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);
}
}
}

View file

@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> GetRandomSongs([FromQuery]Request request)
public async Task<IActionResult> 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<IActionResult> GetUser([FromQuery]Request request, string username)
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> GetIndexes([FromQuery]Request request, string musicFolderId = null, long? ifModifiedSince = null)
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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("<subsonic-response xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://subsonic.org/restapi\" status=\"ok\" version=\"1.16.0\" />", "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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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("<subsonic-response xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://subsonic.org/restapi\" status=\"ok\" version=\"1.16.0\" />", "application/xml");
}
/// <summary>
@ -239,16 +302,16 @@ namespace Roadie.Api.Controllers
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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> 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($"<?xml version=\"1.0\" encoding=\"UTF-8\"?><subsonic-response xmlns=\"http://subsonic.org/restapi\" status=\"failed\" version=\"{ version }\"><error code=\"{ errorCode }\" message=\"{ errorDescription }\"/></subsonic-response>");
}
private IActionResult BuildResponse(Request request, SubsonicOperationResult<Response> 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);

View file

@ -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))

View file

@ -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<OperationResult<Response>> GetAlbum(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<SubsonicOperationResult<Response>> GetAlbum(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<OperationResult<Response>> GetAlbumList(Request request, Roadie.Library.Models.Users.User roadieUser, AlbumListVersions version);
Task<SubsonicOperationResult<Response>> GetAlbumList(Request request, Roadie.Library.Models.Users.User roadieUser, AlbumListVersions version);
Task<OperationResult<Response>> GetArtistInfo(Request request, string id, int? count, bool includeNotPresent, ArtistInfoVersion version);
Task<SubsonicOperationResult<Response>> GetArtistInfo(Request request, int? count, bool includeNotPresent, ArtistInfoVersion version);
Task<OperationResult<Response>> GetArtists(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<SubsonicOperationResult<Response>> GetArtists(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<FileOperationResult<Roadie.Library.Models.Image>> GetCoverArt(Request request, int? size);
Task<SubsonicFileOperationResult<Roadie.Library.Models.Image>> GetCoverArt(Request request, int? size);
Task<OperationResult<Response>> GetGenres(Request request);
Task<SubsonicOperationResult<Response>> GetGenres(Request request);
Task<OperationResult<Response>> GetIndexes(Request request, Roadie.Library.Models.Users.User roadieUser, string musicFolderId = null, long? ifModifiedSince = null);
Task<SubsonicOperationResult<Response>> GetIndexes(Request request, Roadie.Library.Models.Users.User roadieUser, long? ifModifiedSince = null);
OperationResult<Response> GetLicense(Request request);
SubsonicOperationResult<Response> GetLicense(Request request);
Task<OperationResult<Response>> GetMusicDirectory(Request request, Roadie.Library.Models.Users.User roadieUser, string id);
SubsonicOperationResult<Response> GetLyrics(Request request, string artistId, string title);
Task<OperationResult<Response>> GetMusicFolders(Request request);
Task<SubsonicOperationResult<Response>> GetMusicDirectory(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<OperationResult<Response>> GetPlaylist(Request request, Roadie.Library.Models.Users.User roadieUser, string id);
Task<SubsonicOperationResult<Response>> GetMusicFolders(Request request);
Task<OperationResult<Response>> GetPlaylists(Request request, Roadie.Library.Models.Users.User roadieUser, string filterToUserName);
Task<SubsonicOperationResult<Response>> GetPlaylist(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<OperationResult<Response>> GetPodcasts(Request request);
Task<SubsonicOperationResult<Response>> GetPlaylists(Request request, Roadie.Library.Models.Users.User roadieUser, string filterToUserName);
Task<OperationResult<Response>> GetRandomSongs(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<SubsonicOperationResult<Response>> GetPodcasts(Request request);
Task<OperationResult<Response>> GetStarred(Request request, Roadie.Library.Models.Users.User roadieUser, StarredVersion version);
Task<SubsonicOperationResult<Response>> GetRandomSongs(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<OperationResult<Response>> GetUser(Request request, string username);
Task<SubsonicOperationResult<Response>> GetStarred(Request request, Roadie.Library.Models.Users.User roadieUser, StarredVersion version);
OperationResult<Response> Ping(Request request);
Task<SubsonicOperationResult<Response>> GetUser(Request request, string username);
Task<OperationResult<Response>> Search(Request request, Roadie.Library.Models.Users.User roadieUser, SearchVersion version);
SubsonicOperationResult<Response> GetVideos(Request request);
SubsonicOperationResult<Response> Ping(Request request);
Task<SubsonicOperationResult<Response>> Search(Request request, Roadie.Library.Models.Users.User roadieUser, SearchVersion version);
Task<SubsonicOperationResult<Response>> GetAlbumInfo(Request request, Roadie.Library.Models.Users.User roadieUser, AlbumInfoVersion version);
Task<SubsonicOperationResult<Response>> GetArtist(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<SubsonicOperationResult<Response>> GetSong(Request request, Roadie.Library.Models.Users.User roadieUser);
}
}

View file

@ -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 }");
}
}
}

View file

@ -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<OperationResult<subsonic.Response>> GetAlbum(subsonic.Request request, User roadieUser)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetAlbum(subsonic.Request request, User roadieUser)
{
if (!request.ReleaseId.HasValue)
{
return new OperationResult<subsonic.Response>(true, $"Invalid Release [{ request.ReleaseId}]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.ReleaseId}]");
}
var release = this.GetRelease(request.ReleaseId.Value);
if (release == null)
{
return new OperationResult<subsonic.Response>(true, $"Invalid Release [{ request.ReleaseId}]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(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<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -113,7 +114,7 @@ namespace Roadie.Api.Services
/// <summary>
/// Returns a list of random, newest, highest rated etc. albums. Similar to the album lists on the home page of the Subsonic web interface.
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetAlbumList(subsonic.Request request, User roadieUser, subsonic.AlbumListVersions version)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetAlbumList(subsonic.Request request, User roadieUser, subsonic.AlbumListVersions version)
{
var releaseResult = new Library.Models.Pagination.PagedResult<ReleaseList>();
@ -136,18 +137,18 @@ namespace Roadie.Api.Services
break;
default:
return new OperationResult<subsonic.Response>($"Unknown Album List Type [{ request.Type}]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown Album List Type [{ request.Type}]");
}
if (!releaseResult.IsSuccess)
{
return new OperationResult<subsonic.Response>(releaseResult.Message);
return new subsonic.SubsonicOperationResult<subsonic.Response>(releaseResult.Message);
}
switch (version)
{
case subsonic.AlbumListVersions.One:
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -163,7 +164,7 @@ namespace Roadie.Api.Services
};
case subsonic.AlbumListVersions.Two:
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -179,30 +180,29 @@ namespace Roadie.Api.Services
};
default:
return new OperationResult<subsonic.Response>($"Unknown AlbumListVersions [{ version }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown AlbumListVersions [{ version }]");
}
}
/// <summary>
/// Returns artist info with biography, image URLs and similar artists, using data from last.fm.
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetArtistInfo(subsonic.Request request, string id, int? count, bool includeNotPresent, subsonic.ArtistInfoVersion version)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> 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<subsonic.Response>(true, $"Invalid ArtistId [{ id }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(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<subsonic.Response>(true, $"Invalid ArtistId [{ id }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid ArtistId [{ request.id }]");
}
switch (version)
{
case subsonic.ArtistInfoVersion.One:
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -215,7 +215,7 @@ namespace Roadie.Api.Services
};
case subsonic.ArtistInfoVersion.Two:
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -228,11 +228,11 @@ namespace Roadie.Api.Services
};
default:
return new OperationResult<subsonic.Response>($"Unknown ArtistInfoVersion [{ version }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown ArtistInfoVersion [{ version }]");
}
}
public async Task<OperationResult<subsonic.Response>> GetArtists(subsonic.Request request, User roadieUser)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetArtists(subsonic.Request request, User roadieUser)
{
var indexes = new List<subsonic.IndexID3>();
// Indexes for Artists alphabetically
@ -249,7 +249,7 @@ namespace Roadie.Api.Services
artist = this.SubsonicArtistID3sForArtists(artistGroup)
});
};
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -268,10 +268,10 @@ namespace Roadie.Api.Services
/// <summary>
/// Returns a cover art image.
/// </summary>
public async Task<FileOperationResult<Roadie.Library.Models.Image>> GetCoverArt(subsonic.Request request, int? size)
public async Task<subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>> GetCoverArt(subsonic.Request request, int? size)
{
var sw = Stopwatch.StartNew();
var result = new FileOperationResult<Roadie.Library.Models.Image>
var result = new subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>
{
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<subsonic.SubsonicFileOperationResult<Image>>();
}
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<subsonic.SubsonicFileOperationResult<Image>>();
}
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<Roadie.Library.Models.Image>(true, $"Invalid CollectionId [{ request.CollectionId}]");
return new subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>(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<Roadie.Library.Models.Image>(true, $"Invalid ReleaseId [{ request.ReleaseId}]");
return new subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>(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<Roadie.Library.Models.Image>(true, $"Invalid PlaylistId [{ request.PlaylistId}]");
return new subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>(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<Roadie.Library.Models.Image>(true, $"Invalid Username [{ request.u}]");
return new subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>(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<Roadie.Library.Models.Image>(result.Messages)
return new subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>(result.Messages)
{
Data = result.Data,
ETag = result.ETag,
@ -354,7 +354,7 @@ namespace Roadie.Api.Services
/// <summary>
/// Returns all genres
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetGenres(subsonic.Request request)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> 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<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -394,10 +394,10 @@ namespace Roadie.Api.Services
/// <param name="request">Query from application.</param>
/// <param name="musicFolderId">If specified, only return artists in the music folder with the given ID.</param>
/// <param name="ifModifiedSince">If specified, only return a result if the artist collection has changed since the given time (in milliseconds since 1 Jan 1970).</param>
public async Task<OperationResult<subsonic.Response>> GetIndexes(subsonic.Request request, User roadieUser, string musicFolderId = null, long? ifModifiedSince = null)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> 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<int>(musicFolderId));
subsonic.MusicFolder musicFolderFilter = !request.MusicFolderId.HasValue ? new subsonic.MusicFolder() : this.MusicFolders().FirstOrDefault(x => x.id == request.MusicFolderId.Value);
var indexes = new List<subsonic.Index>();
if (musicFolderFilter.id == this.CollectionMusicFolder().id)
@ -443,7 +443,7 @@ namespace Roadie.Api.Services
});
};
}
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -462,9 +462,9 @@ namespace Roadie.Api.Services
/// <summary>
/// Get details about the software license. Takes no extra parameters. Roadies gives everyone a premium 1 year license everytime they ask :)
/// </summary>
public OperationResult<subsonic.Response> GetLicense(subsonic.Request request)
public subsonic.SubsonicOperationResult<subsonic.Response> GetLicense(subsonic.Request request)
{
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -489,7 +489,7 @@ namespace Roadie.Api.Services
/// <param name="request">Query from application.</param>
/// <param name="id">A string which uniquely identifies the music folder. Obtained by calls to getIndexes or getMusicDirectory.</param>
/// <returns></returns>
public async Task<OperationResult<subsonic.Response>> GetMusicDirectory(subsonic.Request request, User roadieUser, string id)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> 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<subsonic.Response>(true, $"Invalid ArtistId [{ request.ArtistId}]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(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<subsonic.Response>(true, $"Invalid CollectionId [{ request.CollectionId}]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(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<subsonic.Response>(true, $"Invalid ReleaseId [{ request.ReleaseId}]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(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<subsonic.Response>($"Unknown GetMusicDirectory Type [{ JsonConvert.SerializeObject(request) }], id [{ id }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown GetMusicDirectory Type [{ JsonConvert.SerializeObject(request) }], id [{ request.id }]");
}
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -573,9 +573,9 @@ namespace Roadie.Api.Services
/// <summary>
/// Returns all configured top-level music folders. Takes no extra parameters.
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetMusicFolders(subsonic.Request request)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetMusicFolders(subsonic.Request request)
{
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -594,23 +594,22 @@ namespace Roadie.Api.Services
/// <summary>
/// Returns a listing of files in a saved playlist.
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetPlaylist(subsonic.Request request, User roadieUser, string id)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetPlaylist(subsonic.Request request, User roadieUser)
{
var playListId = SafeParser.ToGuid(id);
if (!playListId.HasValue)
if (!request.PlaylistId.HasValue)
{
return new OperationResult<subsonic.Response>(true, $"Invalid PlaylistId [{ id }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(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<subsonic.Response>(true, $"Invalid PlaylistId [{ id }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid PlaylistId [{ request.id }]");
}
var tracksForPlaylist = await this.TrackService.List(roadieUser, pagedRequest);
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -626,7 +625,7 @@ namespace Roadie.Api.Services
/// <summary>
/// Returns all playlists a user is allowed to play.
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetPlaylists(subsonic.Request request, User roadieUser, string filterToUserName)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> 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<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -673,9 +672,9 @@ namespace Roadie.Api.Services
/// <summary>
/// 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.
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetPodcasts(subsonic.Request request)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetPodcasts(subsonic.Request request)
{
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -691,13 +690,13 @@ namespace Roadie.Api.Services
/// <summary>
/// Returns random songs matching the given criteria.
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetRandomSongs(subsonic.Request request, User roadieUser)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetRandomSongs(subsonic.Request request, User roadieUser)
{
var songs = new List<subsonic.Child>();
var randomSongs = await this.TrackService.List(roadieUser, request.PagedRequest, true);
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -716,7 +715,7 @@ namespace Roadie.Api.Services
/// <summary>
/// Returns starred songs, albums and artists.
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetStarred(subsonic.Request request, User roadieUser, subsonic.StarredVersion version)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> 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<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -746,7 +745,7 @@ namespace Roadie.Api.Services
};
case subsonic.StarredVersion.Two:
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -764,21 +763,21 @@ namespace Roadie.Api.Services
};
default:
return new OperationResult<subsonic.Response>($"Unknown StarredVersion [{ version }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown StarredVersion [{ version }]");
}
}
/// <summary>
/// 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.
/// </summary>
public async Task<OperationResult<subsonic.Response>> GetUser(subsonic.Request request, string username)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetUser(subsonic.Request request, string username)
{
var user = this.GetUser(username);
if (user == null)
{
return new OperationResult<subsonic.Response>(true, $"Invalid Username [{ username }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Username [{ username }]");
}
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -794,9 +793,9 @@ namespace Roadie.Api.Services
/// <summary>
/// Used to test connectivity with the server. Takes no extra parameters.
/// </summary>
public OperationResult<subsonic.Response> Ping(subsonic.Request request)
public subsonic.SubsonicOperationResult<subsonic.Response> Ping(subsonic.Request request)
{
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -810,7 +809,7 @@ namespace Roadie.Api.Services
/// <summary>
/// Returns albums, artists and songs matching the given search criteria. Supports paging through the result.
/// </summary>
public async Task<OperationResult<subsonic.Response>> Search(subsonic.Request request, User roadieUser, subsonic.SearchVersion version)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> 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<subsonic.Response>("Deprecated since 1.4.0, use search2 instead.");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleClientRestProtocolVersion,"Deprecated since 1.4.0, use search2 instead.");
case subsonic.SearchVersion.Two:
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -863,7 +862,7 @@ namespace Roadie.Api.Services
};
case subsonic.SearchVersion.Three:
return new OperationResult<subsonic.Response>
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
@ -881,11 +880,287 @@ namespace Roadie.Api.Services
};
default:
return new OperationResult<subsonic.Response>($"Unknown SearchVersion [{ version }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion,$"Unknown SearchVersion [{ version }]");
}
}
public subsonic.ArtistInfo2 SubsonicArtistInfo2InfoForArtist(data.Artist artist)
/// <summary>
/// Returns album notes, image URLs etc, using data from last.fm.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetAlbumInfo(subsonic.Request request, User roadieUser, subsonic.AlbumInfoVersion version)
{
if (!request.ReleaseId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]");
}
var release = this.GetRelease(request.ReleaseId.Value);
if (release == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]");
}
switch (version)
{
case subsonic.AlbumInfoVersion.One:
case subsonic.AlbumInfoVersion.Two:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
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.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown Album Info Version [{ request.Type}]");
}
}
/// <summary>
/// Returns details for an artist, including a list of albums. This method organizes music according to ID3 tags.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetArtist(subsonic.Request request, User roadieUser)
{
if (!request.ArtistId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(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.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]");
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.artist,
Item = this.SubsonicArtistForArtist(artist)
}
};
}
/// <summary>
/// Returns details for a song.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetSong(subsonic.Request request, User roadieUser)
{
if (!request.TrackId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(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.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track [{ request.id }]");
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.song,
Item = this.SubsonicChildForTrack(track)
}
};
}
/// <summary>
/// Returns top songs for the given artist, using data from last.fm.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetTopSongs(subsonic.Request request, User roadieUser, string artistId, int? count = 50)
{
// TODO
throw new NotImplementedException();
}
/// <summary>
/// Returns songs in a given genre.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetSongsByGenre(subsonic.Request request, User roadieUser, string genre, int? count = 10, int? offset = 0)
{
// TODO
throw new NotImplementedException();
}
/// <summary>
/// Returns all video files.
/// </summary>
public subsonic.SubsonicOperationResult<subsonic.Response> GetVideos(subsonic.Request request)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
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]
}
}
};
}
/// <summary>
/// Searches for and returns lyrics for a given song
/// </summary>
public subsonic.SubsonicOperationResult<subsonic.Response> GetLyrics(subsonic.Request request, string artistId, string title)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
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]
}
}
};
}
/// <summary>
/// Returns a random collection of songs from the given artist and similar artists, using data from last.fm. Typically used for artist radio features.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetSimliarSongs(subsonic.Request request, User roadieUser, subsonic.SimilarSongsVersion version, int? count = 50)
{
// TODO
throw new NotImplementedException();
}
/// <summary>
/// Downloads a given media file. Similar to stream, but this method returns the original media data without transcoding or downsampling.
/// </summary>
public async Task<subsonic.SubsonicFileOperationResult<subsonic.Response>> Download(subsonic.Request request, User roadieUser)
{
// TODO
throw new NotImplementedException();
}
/// <summary>
/// Authenticate the given credentials and return the corresponding ApplicationUser
/// </summary>
public async Task<subsonic.SubsonicOperationResult<ApplicationUser>> Authenticate(subsonic.Request request, string username, string password)
{
// TODO
//public user CheckPasswordGetUser(ICacheManager<object> 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<user>(cacheKey);
// if (resultInCache == null)
// {
// user = context.users.FirstOrDefault(x => x.username.Equals(this.UsernameValue, StringComparison.OrdinalIgnoreCase));
// var claims = new List<string>
// {
// 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<string>(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
}
}

View file

@ -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)))

View file

@ -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<T>(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();
}
}
}

View file

@ -324,5 +324,6 @@ namespace Roadie.Library.Extensions
}
return input;
}
}
}

View file

@ -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; }

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Roadie.Library.Models.ThirdPartyApi.Subsonic
{
public enum AlbumInfoVersion
{
One,
Two
}
}

View file

@ -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
}
}

View file

@ -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<object> 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<user>(cacheKey);
// if (resultInCache == null)
// {
// user = context.users.FirstOrDefault(x => x.username.Equals(this.UsernameValue, StringComparison.OrdinalIgnoreCase));
// var claims = new List<string>
// {
// 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<string>(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;
//}
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Roadie.Library.Models.ThirdPartyApi.Subsonic
{
public enum SimilarSongsVersion
{
One,
Two
}
}

View file

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Roadie.Library.Models.ThirdPartyApi.Subsonic
{
public class SubsonicFileOperationResult<T> : FileOperationResult<T>
{
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<string> messages = null)
: base(messages)
{
}
public SubsonicFileOperationResult(bool isNotFoundResult, IEnumerable<string> messages = null)
: base(isNotFoundResult, messages)
{
}
}
}

View file

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
namespace Roadie.Library.Models.ThirdPartyApi.Subsonic
{
[Serializable]
public class SubsonicOperationResult<T> : OperationResult<T>
{
public ErrorCodes? ErrorCode { get; set; }
public SubsonicOperationResult(bool isNotFoundResult, IEnumerable<string> messages = null)
: base(isNotFoundResult, messages)
{
}
public SubsonicOperationResult()
{
}
public SubsonicOperationResult(IEnumerable<string> 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)
{
}
}
}

View file

@ -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;
}