using Mapster;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Roadie.Api.ModelBinding;
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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace Roadie.Api.Controllers
{
[Route("subsonic/rest")]
[ApiController]
public class SubsonicController : EntityControllerBase
{
private IPlayActivityService PlayActivityService { get; }
private ISubsonicService SubsonicService { get; }
///
/// This is the user authenticated via the Subsonic methods - NOT the current API Identity User.
///
private Library.Models.Users.User SubsonicUser { get; set; }
private ITrackService TrackService { get; }
private IReleaseService ReleaseService { get; }
public SubsonicController(ISubsonicService subsonicService, ITrackService trackService, IReleaseService releaseService, IPlayActivityService playActivityService, ILoggerFactory logger, ICacheManager cacheManager, IConfiguration configuration, UserManager userManager)
: base(cacheManager, configuration, userManager)
{
this.Logger = logger.CreateLogger("RoadieApi.Controllers.SubsonicController");
this.SubsonicService = subsonicService;
this.TrackService = trackService;
this.ReleaseService = releaseService;
this.PlayActivityService = playActivityService;
}
[HttpGet("getAlbum.view")]
[HttpPost("getAlbum.view")]
[ProducesResponseType(200)]
public async Task GetAlbum(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetAlbum(request, this.SubsonicUser);
return this.BuildResponse(request, result, "album");
}
[HttpGet("createBookmark.view")]
[HttpPost("createBookmark.view")]
[ProducesResponseType(200)]
public async Task CreateBookmark(SubsonicRequest request, int position, string comment)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.CreateBookmark(request, this.SubsonicUser, position, comment);
return this.BuildResponse(request, result);
}
[HttpGet("deleteBookmark.view")]
[HttpPost("deleteBookmark.view")]
[ProducesResponseType(200)]
public async Task DeleteBookmark(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.DeleteBookmark(request, this.SubsonicUser);
return this.BuildResponse(request, result);
}
[HttpGet("deletePlaylist.view")]
[HttpPost("deletePlaylist.view")]
[ProducesResponseType(200)]
public async Task DeletePlaylist(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.DeletePlaylist(request, this.SubsonicUser);
return this.BuildResponse(request, result);
}
[HttpGet("updatePlaylist.view")]
[HttpPost("updatePlaylist.view")]
[ProducesResponseType(200)]
public async Task UpdatePlaylist(SubsonicRequest request, string playlistId, string name, string comment, bool? @public, string[] songIdToAdd, int[] songIndexToRemove)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.UpdatePlaylist(request, this.SubsonicUser, playlistId, name, comment, @public, songIdToAdd, songIndexToRemove);
return this.BuildResponse(request, result);
}
[HttpGet("getBookmarks.view")]
[HttpPost("getBookmarks.view")]
[ProducesResponseType(200)]
public async Task GetBookmarks(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetBookmarks(request, this.SubsonicUser);
return this.BuildResponse(request, result, "bookmarks");
}
[HttpGet("star.view")]
[HttpPost("star.view")]
[ProducesResponseType(200)]
public async Task Star(SubsonicRequest request, string[] albumId, string[] artistId)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.ToggleStar(request, this.SubsonicUser, true, albumId, artistId);
return this.BuildResponse(request, result);
}
[HttpGet("unstar.view")]
[HttpPost("unstar.view")]
[ProducesResponseType(200)]
public async Task UnStar(SubsonicRequest request, string[] albumId, string[] artistId)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.ToggleStar(request, this.SubsonicUser, false, albumId, artistId);
return this.BuildResponse(request, result);
}
[HttpGet("setRating.view")]
[HttpPost("setRating.view")]
[ProducesResponseType(200)]
public async Task SetRating(SubsonicRequest request, short rating)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.SetRating(request, this.SubsonicUser, rating);
return this.BuildResponse(request, result);
}
[HttpGet("getAlbumInfo.view")]
[HttpPost("getAlbumInfo.view")]
[ProducesResponseType(200)]
public async Task GetAlbumInfo(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetAlbumInfo(request, this.SubsonicUser, AlbumInfoVersion.One);
return this.BuildResponse(request, result, "albumInfo");
}
[HttpGet("getAlbumInfo2.view")]
[HttpPost("getAlbumInfo2.view")]
[ProducesResponseType(200)]
public async Task GetAlbumInfo2(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetAlbumInfo(request, this.SubsonicUser, AlbumInfoVersion.Two);
return this.BuildResponse(request, result, "albumInfo");
}
[HttpGet("getAlbumList.view")]
[HttpPost("getAlbumList.view")]
[ProducesResponseType(200)]
public async Task GetAlbumList(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetAlbumList(request, this.SubsonicUser, AlbumListVersions.One);
return this.BuildResponse(request, result, "albumList");
}
[HttpGet("getAlbumList2.view")]
[HttpPost("getAlbumList2.view")]
[ProducesResponseType(200)]
public async Task GetAlbumList2(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetAlbumList(request, this.SubsonicUser, AlbumListVersions.Two);
return this.BuildResponse(request, result, "albumList");
}
[HttpGet("getArtist.view")]
[HttpPost("getArtist.view")]
[ProducesResponseType(200)]
public async Task GetArtist(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetArtist(request, this.SubsonicUser);
return this.BuildResponse(request, result, "artist");
}
[HttpGet("getArtistInfo.view")]
[HttpPost("getArtistInfo.view")]
[ProducesResponseType(200)]
public async Task GetArtistInfo(SubsonicRequest request, int? count, bool? includeNotPresent)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
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(SubsonicRequest request, int? count, bool? includeNotPresent)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetArtistInfo(request, count, includeNotPresent ?? false, ArtistInfoVersion.Two);
return this.BuildResponse(request, result, "artistInfo2");
}
[HttpGet("getArtists.view")]
[HttpPost("getArtists.view")]
[ProducesResponseType(200)]
public async Task GetArtists(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetArtists(request, this.SubsonicUser);
return this.BuildResponse(request, result, "artists");
}
[HttpGet("getAvatar.view")]
[HttpPost("getAvatar.view")]
[ProducesResponseType(200)]
public async Task GetAvatar(SubsonicRequest request, string username)
{
var user = await this.UserManager.FindByNameAsync(username);
return Redirect($"/images/user/{ user.RoadieId }/{this.RoadieSettings.ThumbnailImageSize.Width}/{this.RoadieSettings.ThumbnailImageSize.Height}");
}
[HttpGet("getCoverArt.view")]
[HttpPost("getCoverArt.view")]
[ProducesResponseType(200)]
public async Task GetCoverArt(SubsonicRequest request, int? size)
{
var result = await this.SubsonicService.GetCoverArt(request, size);
if (result == null || result.IsNotFoundResult)
{
return NotFound();
}
if (!result.IsSuccess)
{
this.Logger.LogWarning($"GetCoverArt Failed For [{ JsonConvert.SerializeObject(request) }]");
return StatusCode((int)HttpStatusCode.InternalServerError);
}
return File(fileContents: result.Data.Bytes,
contentType: result.ContentType,
fileDownloadName: $"{ result.Data.Caption ?? request.id.ToString()}.jpg",
lastModified: result.LastModified,
entityTag: result.ETag);
}
[HttpGet("getGenres.view")]
[HttpPost("getGenres.view")]
[ProducesResponseType(200)]
public async Task GetGenres(SubsonicRequest request)
{
var result = await this.SubsonicService.GetGenres(request);
return this.BuildResponse(request, result, "genres");
}
[HttpGet("getIndexes.view")]
[HttpPost("getIndexes.view")]
[ProducesResponseType(200)]
public async Task GetIndexes(SubsonicRequest request, long? ifModifiedSince = null)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetIndexes(request, this.SubsonicUser, ifModifiedSince);
return this.BuildResponse(request, result, "indexes");
}
[HttpGet("getLicense.view")]
[HttpPost("getLicense.view")]
[ProducesResponseType(200)]
public IActionResult GetLicense(SubsonicRequest request)
{
var result = this.SubsonicService.GetLicense(request);
return this.BuildResponse(request, result, "license");
}
[HttpGet("getLyrics.view")]
[HttpPost("getLyrics.view")]
[ProducesResponseType(200)]
public IActionResult GetLyrics(SubsonicRequest request, string artist, string title)
{
var result = this.SubsonicService.GetLyrics(request, artist, title);
return this.BuildResponse(request, result, "lyrics ");
}
[HttpGet("getMusicDirectory.view")]
[HttpPost("getMusicDirectory.view")]
[ProducesResponseType(200)]
public async Task GetMusicDirectory(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetMusicDirectory(request, this.SubsonicUser);
return this.BuildResponse(request, result, "directory");
}
[HttpGet("getMusicFolders.view")]
[HttpPost("getMusicFolders.view")]
[ProducesResponseType(200)]
public async Task GetMusicFolders(SubsonicRequest 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(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetPlaylist(request, this.SubsonicUser);
return this.BuildResponse(request, result, "playlist");
}
[HttpGet("createPlaylist.view")]
[HttpPost("createPlaylist.view")]
[ProducesResponseType(200)]
public async Task CreatePlaylist(SubsonicRequest request, string playlistId, string name, string[] songId)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.CreatePlaylist(request, this.SubsonicUser, name, songId, playlistId);
return this.BuildResponse(request, result, "playlist");
}
[HttpGet("getPlaylists.view")]
[HttpPost("getPlaylists.view")]
[ProducesResponseType(200)]
public async Task GetPlaylists(SubsonicRequest request, string username)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetPlaylists(request, this.SubsonicUser, username);
return this.BuildResponse(request, result, "playlists");
}
[HttpGet("getPodcasts.view")]
[HttpPost("getPodcasts.view")]
[ProducesResponseType(200)]
public async Task GetPodcasts(SubsonicRequest request, bool includeEpisodes)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
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(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetRandomSongs(request, this.SubsonicUser);
return this.BuildResponse(request, result, "randomSongs");
}
[HttpGet("getSimilarSongs.view")]
[HttpPost("getSimilarSongs.view")]
[ProducesResponseType(200)]
public async Task GetSimilarSongs(SubsonicRequest request, int? count = 50)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetSimliarSongs(request, this.SubsonicUser, SimilarSongsVersion.One, count);
return this.BuildResponse(request, result, "similarSongs");
}
[HttpGet("getSimilarSongs2.view")]
[HttpPost("getSimilarSongs2.view")]
[ProducesResponseType(200)]
public async Task GetSimilarSongs2(SubsonicRequest request, int? count = 50)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetSimliarSongs(request, this.SubsonicUser, SimilarSongsVersion.Two, count);
return this.BuildResponse(request, result, "similarSongs2");
}
[HttpGet("getSong.view")]
[HttpPost("getSong.view")]
[ProducesResponseType(200)]
public async Task GetSong(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetSong(request, this.SubsonicUser);
return this.BuildResponse(request, result, "song");
}
[HttpGet("getSongsByGenre.view")]
[HttpPost("getSongsByGenre.view")]
[ProducesResponseType(200)]
public async Task GetSongsByGenre(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetSongsByGenre(request, this.SubsonicUser);
return this.BuildResponse(request, result, "songsByGenre");
}
[HttpGet("getStarred.view")]
[HttpPost("getStarred.view")]
[ProducesResponseType(200)]
public async Task GetStarred(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetStarred(request, this.SubsonicUser, StarredVersion.One);
return this.BuildResponse(request, result, "starred");
}
[HttpGet("getStarred2.view")]
[HttpPost("getStarred2.view")]
[ProducesResponseType(200)]
public async Task GetStarred2(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetStarred(request, this.SubsonicUser, StarredVersion.Two);
return this.BuildResponse(request, result, "starred");
}
[HttpGet("getTopSongs.view")]
[HttpPost("getTopSongs.view")]
[ProducesResponseType(200)]
public async Task GetTopSongs(SubsonicRequest request, int? count = 50)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetTopSongs(request, this.SubsonicUser, count);
return this.BuildResponse(request, result, "topSongs");
}
[HttpGet("getUser.view")]
[HttpPost("getUser.view")]
[ProducesResponseType(200)]
public async Task GetUser(SubsonicRequest request, string username)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetUser(request, username ?? request.u);
return this.BuildResponse(request, result, "user");
}
[HttpGet("getVideos.view")]
[HttpPost("getVideos.view")]
[ProducesResponseType(200)]
public IActionResult GetVideos(SubsonicRequest request)
{
var result = this.SubsonicService.GetVideos(request);
return this.BuildResponse(request, result, "videos");
}
[HttpGet("ping.view")]
[HttpPost("ping.view")]
[ProducesResponseType(200)]
public async Task Ping(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
if (request.IsJSONRequest)
{
var result = this.SubsonicService.Ping(request);
return this.BuildResponse(request, result);
}
return Content("", "application/xml");
}
///
/// Returns albums, artists and songs matching the given search criteria. Supports paging through the result.
///
[HttpGet("search.view")]
[HttpPost("search.view")]
[ProducesResponseType(200)]
public async Task Search(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.Search(request, this.SubsonicUser, SearchVersion.One);
return this.BuildResponse(request, result, "searchResult");
}
[HttpGet("search2.view")]
[HttpPost("search2.view")]
[ProducesResponseType(200)]
public async Task Search2(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.Search(request, this.SubsonicUser, SearchVersion.Two);
return this.BuildResponse(request, result, "searchResult2");
}
[HttpGet("search3.view")]
[HttpPost("search3.view")]
[ProducesResponseType(200)]
public async Task Search3(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.Search(request, this.SubsonicUser, SearchVersion.Three);
return this.BuildResponse(request, result, "searchResult3");
}
[HttpGet("stream.view")]
[HttpPost("stream.view")]
[ProducesResponseType(200)]
public async Task StreamTrack(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return Unauthorized();
}
var trackId = request.TrackId;
if (trackId == null)
{
return NotFound("Invalid TrackId");
}
return await base.StreamTrack(trackId.Value, this.TrackService, this.PlayActivityService, this.SubsonicUser);
}
[HttpGet("download.view")]
[HttpPost("download.view")]
[ProducesResponseType(200)]
public async Task Download(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return Unauthorized();
}
var trackId = request.TrackId;
if (trackId != null)
{
return await base.StreamTrack(trackId.Value, this.TrackService, this.PlayActivityService, this.SubsonicUser);
}
var releaseId = request.ReleaseId;
if(releaseId != null)
{
var releaseZip = await this.ReleaseService.ReleaseZipped(this.SubsonicUser, releaseId.Value);
if(!releaseZip.IsSuccess)
{
return NotFound("Unknown Release id");
}
return File(releaseZip.Data, "application/zip",(string)releaseZip.AdditionalData["ZipFileName"]);
}
return NotFound($"Unknown download id `{ request.id }`");
}
private async Task AuthenticateUser(SubsonicRequest request)
{
var appUser = await this.SubsonicService.Authenticate(request);
if (!(appUser?.IsSuccess ?? false) || (appUser?.IsNotFoundResult ?? false))
{
return this.BuildResponse(request, appUser.Adapt>());
}
this.SubsonicUser = this.UserModelForUser(appUser.Data.User);
return null;
}
#region Response Builder Methods
private IActionResult BuildResponse(SubsonicRequest request, SubsonicOperationResult response = null, string responseType = null)
{
var acceptHeader = this.Request.Headers["Accept"];
string postBody = null;
string queryString = this.Request.QueryString.ToString();
string queryPath = this.Request.Path;
string method = this.Request.Method;
if (!this.Request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(this.Request.ContentType))
{
var formCollection = this.Request.Form;
var formDictionary = new Dictionary();
if (formCollection != null && formCollection.Any())
{
foreach (var form in formCollection)
{
if (!formDictionary.ContainsKey(form.Key))
{
formDictionary[form.Key] = form.Value.FirstOrDefault();
}
}
}
postBody = JsonConvert.SerializeObject(formDictionary);
}
this.Logger.LogTrace($"Subsonic Request: Method [{ method }], Accept Header [{ acceptHeader }], Path [{ queryPath }], Query String [{ queryString }], Posted Body [{ postBody }], Response Error Code [{ response?.ErrorCode }], Request [{ JsonConvert.SerializeObject(request, Formatting.Indented) }] ResponseType [{ responseType }]");
if (response?.ErrorCode.HasValue ?? false)
{
return this.SendError(request, response);
}
if (request.IsJSONRequest)
{
this.Response.ContentType = "application/json";
var status = response?.Data?.status.ToString();
var version = response?.Data?.version ?? Roadie.Api.Services.SubsonicService.SubsonicVersion;
string jsonResult = null;
if (responseType == null)
{
jsonResult = "{ \"subsonic-response\": { \"status\":\"" + status + "\", \"version\": \"" + version + "\" }}";
}
else
{
jsonResult = "{ \"subsonic-response\": { \"status\":\"" + status + "\", \"version\": \"" + version + "\", \"" + responseType + "\":" + (response?.Data != null ? JsonConvert.SerializeObject(response.Data.Item) : string.Empty) + "}}";
}
if ((request?.f ?? string.Empty).Equals("jsonp", StringComparison.OrdinalIgnoreCase))
{
jsonResult = request.callback + "(" + jsonResult + ");";
}
return Content(jsonResult);
}
this.Response.ContentType = "application/xml";
return Ok(response.Data);
}
private IActionResult SendError(SubsonicRequest request, SubsonicOperationResult response = null)
{
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("{ \"subsonic-response\": { \"status\":\"failed\", \"version\": \"" + version + "\", \"error\":{\"code\":\"" + errorCode + "\",\"message\":\"" + errorDescription + "\"}}}");
}
this.Response.ContentType = "application/xml";
return Content($"");
}
#endregion Response Builder Methods
}
}