Subsonic API Work

This commit is contained in:
Steven Hildreth 2018-11-23 19:46:12 -06:00
parent f56cfb9cbd
commit 969393128c
14 changed files with 195 additions and 77 deletions

View file

@ -10,6 +10,7 @@ using Roadie.Library.Configuration;
using Roadie.Library.Identity;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using models = Roadie.Library.Models.Users;
@ -56,20 +57,36 @@ namespace Roadie.Api.Controllers
return result;
}
protected async Task<FileStreamResult> StreamTrack(Guid id, ITrackService trackService, IPlayActivityService playActivityService, models.User currentUser = null)
protected async Task<IActionResult> StreamTrack(Guid id, ITrackService trackService, IPlayActivityService playActivityService, models.User currentUser = null)
{
var user = currentUser ?? await this.CurrentUserModel();
var track = await trackService.ById(user, id, null);
if (track == null || track.IsNotFoundResult)
if (track == null || ( track?.IsNotFoundResult ?? false))
{
Response.StatusCode = (int)HttpStatusCode.NotFound;
if (track?.Errors != null && (track?.Errors.Any() ?? false))
{
this.Logger.LogCritical($"StreamTrack: ById Invalid For TrackId [{ id }] OperationResult Errors [{ string.Join('|', track?.Errors ?? new Exception[0]) }], For User [{ currentUser }]");
}
else
{
this.Logger.LogCritical($"StreamTrack: ById Invalid For TrackId [{ id }] OperationResult Messages [{ string.Join('|', track?.Messages ?? new string[0]) }], For User [{ currentUser }]");
}
return NotFound("Unknown TrackId");
}
var info = await trackService.TrackStreamInfo(id,
Services.TrackService.DetermineByteStartFromHeaders(this.Request.Headers),
Services.TrackService.DetermineByteEndFromHeaders(this.Request.Headers, track.Data.FileSize));
if (!info.IsSuccess)
if (!info?.IsSuccess ?? false || info?.Data == null)
{
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
if (info?.Errors != null && (info?.Errors.Any() ?? false))
{
this.Logger.LogCritical($"StreamTrack: TrackStreamInfo Invalid For TrackId [{ id }] OperationResult Errors [{ string.Join('|', info?.Errors ?? new Exception[0]) }], For User [{ currentUser }]");
}
else
{
this.Logger.LogCritical($"StreamTrack: TrackStreamInfo Invalid For TrackId [{ id }] OperationResult Messages [{ string.Join('|', info?.Messages ?? new string[0]) }], For User [{ currentUser }]");
}
return NotFound("Unknown TrackId");
}
Response.Headers.Add("Content-Disposition", info.Data.ContentDisposition);
Response.Headers.Add("X-Content-Duration", info.Data.ContentDuration);
@ -87,7 +104,7 @@ namespace Roadie.Api.Controllers
Response.Headers.Add("Expires", info.Data.Expires);
var stream = new MemoryStream(info.Data.Bytes);
var playListUser = await playActivityService.CreatePlayActivity(user, info.Data);
this.Logger.LogInformation($"StreamTrack PlayActivity `{ playListUser?.ToString() }`, StreamInfo `{ info.Data.ToString() }`");
this.Logger.LogInformation($"StreamTrack PlayActivity `{ playListUser?.Data.ToString() }`, StreamInfo `{ info.Data.ToString() }`");
return new FileStreamResult(stream, info.Data.ContentType)
{
FileDownloadName = info.Data.FileName

View file

@ -66,7 +66,7 @@ namespace Roadie.Api.Controllers
}
[HttpGet("track/{id}.{mp3?}")]
public async Task<FileStreamResult> StreamTrack(Guid id)
public async Task<IActionResult> StreamTrack(Guid id)
{
return await base.StreamTrack(id, this.TrackService, this.PlayActivityService);
}

View file

@ -11,6 +11,9 @@ 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;
@ -265,14 +268,6 @@ namespace Roadie.Api.Controllers
[ProducesResponseType(200)]
public async Task<IActionResult> GetPlaylist(SubsonicRequest request)
{
this.Logger.Log(LogLevel.Critical, ":: Critial");
this.Logger.Log(LogLevel.Debug, ":: Debug");
this.Logger.Log(LogLevel.Error, ":: Error");
this.Logger.Log(LogLevel.Information, ":: Information");
this.Logger.Log(LogLevel.None, ":: None");
this.Logger.Log(LogLevel.Trace, ":: Trace");
this.Logger.Log(LogLevel.Warning, ":: Warning");
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
@ -438,8 +433,13 @@ namespace Roadie.Api.Controllers
[HttpGet("ping.view")]
[HttpPost("ping.view")]
[ProducesResponseType(200)]
public IActionResult Ping(SubsonicRequest request)
public async Task<IActionResult> Ping(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
if (request.IsJSONRequest)
{
var result = this.SubsonicService.Ping(request);
@ -496,17 +496,18 @@ namespace Roadie.Api.Controllers
[HttpGet("stream.view")]
[HttpPost("stream.view")]
[ProducesResponseType(200)]
public async Task<FileStreamResult> StreamTrack(SubsonicRequest request)
public async Task<IActionResult> StreamTrack(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Unauthorized();
}
var trackId = request.TrackId;
if (trackId == null)
{
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return NotFound("Invalid TrackId");
}
return await base.StreamTrack(trackId.Value, this.TrackService, this.PlayActivityService, this.SubsonicUser);
}
@ -527,7 +528,28 @@ namespace Roadie.Api.Controllers
private IActionResult BuildResponse(SubsonicRequest 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 }]");
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<string, object>();
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) }] ResponseType [{ responseType }]");
if (response?.ErrorCode.HasValue ?? false)
{
return this.SendError(request, response);

View file

@ -89,6 +89,10 @@ namespace Roadie.Api.ModelBinding
{
modelDictionary[form.Key] = form.Value.FirstOrDefault();
}
else
{
modelDictionary.Add(form.Key, form.Value.FirstOrDefault());
}
}
}
}

View file

@ -24,7 +24,8 @@
<PackageReference Include="Serilog.Exceptions" Version="4.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.RollingFileAlternate" Version="2.0.9" />
<PackageReference Include="Serilog.Sinks.SQLite" Version="4.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.3.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.9" />
</ItemGroup>

View file

@ -206,7 +206,11 @@ namespace Roadie.Api.Services
}
if (includes.Contains("playlists"))
{
var r = await this.PlaylistService.List(request: new PagedRequest(), artistId: artist.RoadieId);
var pg = new PagedRequest
{
FilterToArtistId = artist.RoadieId
};
var r = await this.PlaylistService.List(pg);
if (r.IsSuccess)
{
result.PlaylistsWithArtistReleases = r.Rows.ToArray();

View file

@ -8,6 +8,6 @@ namespace Roadie.Api.Services
{
public interface IPlaylistService
{
Task<PagedResult<PlaylistList>> List(PagedRequest request, User roadieUser = null, Guid? artistId = null);
Task<PagedResult<PlaylistList>> List(PagedRequest request, User roadieUser = null);
}
}

View file

@ -222,10 +222,18 @@ namespace Roadie.Api.Services
try
{
// See if artist images exists in artist folder
var artistImages = Directory.GetFiles(artist.ArtistFileFolder(this.Configuration, this.Configuration.LibraryFolder), "artist*.*");
if (artistImages.Any())
artistFolder = artist.ArtistFileFolder(this.Configuration, this.Configuration.LibraryFolder);
if (!Directory.Exists(artistFolder))
{
imageBytes = File.ReadAllBytes(artistImages.First());
this.Logger.LogWarning($"Artist Folder [{ artistFolder }], Not Found For Artist [{ artist.ToString() }]");
}
else
{
var artistImages = Directory.GetFiles(artistFolder, "artist*.*");
if (artistImages.Any())
{
imageBytes = File.ReadAllBytes(artistImages.First());
}
}
}
catch (Exception ex)
@ -424,19 +432,35 @@ namespace Roadie.Api.Services
}
byte[] imageBytes = null;
string artistFolder = null;
string releaseFolder = null;
try
{
// See if cover art file exists in release folder
artistFolder = release.Artist.ArtistFileFolder(this.Configuration, this.Configuration.LibraryFolder);
var coverArtFiles = Directory.GetFiles(release.ReleaseFileFolder(artistFolder), "cover*.*");
if (coverArtFiles.Any())
if (!Directory.Exists(artistFolder))
{
imageBytes = File.ReadAllBytes(coverArtFiles.First());
this.Logger.LogWarning($"Artist Folder [{ artistFolder }], Not Found For Artist [{ release.Artist.ToString() }]");
}
else
{
releaseFolder = release.ReleaseFileFolder(artistFolder);
if (!Directory.Exists(releaseFolder))
{
this.Logger.LogWarning($"Release Folder [{ releaseFolder }], Not Found For Release [{ release.ToString() }]");
}
else
{
var coverArtFiles = Directory.GetFiles(releaseFolder, "cover*.*");
if (coverArtFiles.Any())
{
imageBytes = File.ReadAllBytes(coverArtFiles.First());
}
}
}
}
catch (Exception ex)
{
this.Logger.LogError(ex, $"Error Reading Folder [{ artistFolder }] For Artist [{ release.Artist.Id }]");
this.Logger.LogError(ex, $"Error Reading Release Folder [{ releaseFolder }] Artist Folder [{ artistFolder }] For Artist [{ release.Artist.Id }]");
}
imageBytes = imageBytes ?? release.Thumbnail;
var image = new data.Image

View file

@ -29,25 +29,33 @@ namespace Roadie.Api.Services
{
}
public async Task<Library.Models.Pagination.PagedResult<PlaylistList>> List(PagedRequest request, User roadieUser = null, Guid? artistId = null)
public async Task<Library.Models.Pagination.PagedResult<PlaylistList>> List(PagedRequest request, User roadieUser = null)
{
var sw = new Stopwatch();
sw.Start();
int[] playlistWithArtistTrackIds = new int[0];
if(request.FilterToArtistId.HasValue)
{
playlistWithArtistTrackIds = (from pl in this.DbContext.Playlists
join pltr in this.DbContext.PlaylistTracks on pl.Id equals pltr.PlayListId
join t in this.DbContext.Tracks on pltr.TrackId equals t.Id
join rm in this.DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id
join r in this.DbContext.Releases on rm.ReleaseId equals r.Id
join a in this.DbContext.Artists on r.ArtistId equals a.Id
where a.RoadieId == request.FilterToArtistId
select pl.Id
).ToArray();
}
var result = (from pl in this.DbContext.Playlists
join pltr in this.DbContext.PlaylistTracks on pl.Id equals pltr.PlayListId
join t in this.DbContext.Tracks on pltr.TrackId equals t.Id
join rm in this.DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id
join r in this.DbContext.Releases on rm.ReleaseId equals r.Id
join a in this.DbContext.Artists on r.ArtistId equals a.Id
join u in this.DbContext.Users on pl.UserId equals u.Id
let duration = (from plt in this.DbContext.PlaylistTracks
join t in this.DbContext.Tracks on plt.TrackId equals t.Id
select t.Duration).Sum()
where (request.FilterToPlaylistId == null || pl.RoadieId == request.FilterToPlaylistId)
where (request.FilterToArtistId == null || a.RoadieId == request.FilterToArtistId)
where (request.FilterToArtistId == null || playlistWithArtistTrackIds.Contains(pl.Id))
where ((roadieUser == null && pl.IsPublic) || (roadieUser != null && u.RoadieId == roadieUser.UserId || pl.IsPublic))
where (artistId == null || (artistId != null && a.RoadieId == artistId))
where (request.FilterValue.Length == 0 || (request.FilterValue.Length > 0 && (
pl.Name != null && pl.Name.Contains(request.FilterValue))
))
@ -57,6 +65,7 @@ namespace Roadie.Api.Services
{
Text = pl.Name,
Value = pl.RoadieId.ToString()
},
User = new DataToken
{
@ -71,7 +80,7 @@ namespace Roadie.Api.Services
UserThumbnail = MakeUserThumbnailImage(u.RoadieId),
Id = pl.RoadieId,
Thumbnail = MakePlaylistThumbnailImage(pl.RoadieId)
}).Distinct();
});
var sortBy = string.IsNullOrEmpty(request.Sort) ? request.OrderValue(new Dictionary<string, string> { { "Playlist.Text", "ASC" } }) : request.OrderValue(null);
var rowCount = result.Count();
var rows = result.OrderBy(sortBy).Skip(request.SkipValue).Take(request.LimitValue).ToArray();

View file

@ -136,6 +136,21 @@ namespace Roadie.Api.Services
where g.Name == request.FilterByGenre
select rg.ReleaseId).ToArray();
}
if(request.FilterFromYear.HasValue || request.FilterToYear.HasValue)
{
// If from is larger than to then reverse values and set sort order to desc
if(request.FilterToYear > request.FilterFromYear)
{
var t = request.FilterToYear;
request.FilterToYear = request.FilterFromYear;
request.FilterFromYear = t;
request.Order = "DESC";
}
else
{
request.Order = "ASC";
}
}
var result = (from r in this.DbContext.Releases.Include("Artist")
join a in this.DbContext.Artists on r.ArtistId equals a.Id
where (request.FilterMinimumRating == null || r.Rating >= request.FilterMinimumRating.Value)

View file

@ -814,6 +814,8 @@ namespace Roadie.Api.Services
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid PlaylistId [{ request.id }]");
}
// For a playlist to show all the tracks in the playlist set the limit to the playlist size
pagedRequest.Limit = playlist.PlaylistCount ?? pagedRequest.Limit;
var tracksForPlaylist = await this.TrackService.List(roadieUser, pagedRequest);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
@ -833,31 +835,37 @@ namespace Roadie.Api.Services
/// </summary>
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
let trackCount = (from pl in this.DbContext.PlaylistTracks
where pl.PlayListId == playlist.Id
select pl.Id).Count()
let playListDuration = (from pl in this.DbContext.PlaylistTracks
join t in this.DbContext.Tracks on pl.TrackId equals t.Id
where pl.PlayListId == playlist.Id
select t.Duration).Sum()
where (playlist.IsPublic) || (roadieUser != null && playlist.UserId == roadieUser.Id)
select new subsonic.Playlist
{
id = playlist.RoadieId.ToString(),
name = playlist.Name,
comment = playlist.Description,
owner = u.UserName,
songCount = trackCount,
duration = playListDuration.ToSecondsFromMilliseconds(),
created = playlist.CreatedDate,
changed = playlist.LastUpdated ?? playlist.CreatedDate,
coverArt = this.MakePlaylistThumbnailImage(playlist.RoadieId).Url,
@public = playlist.IsPublic,
publicSpecified = playlist.IsPublic
}
);
//var playlists = (from playlist in this.DbContext.Playlists
// join u in this.DbContext.Users on playlist.UserId equals u.Id
// let trackCount = (from pl in this.DbContext.PlaylistTracks
// where pl.PlayListId == playlist.Id
// select pl.Id).Count()
// let playListDuration = (from pl in this.DbContext.PlaylistTracks
// join t in this.DbContext.Tracks on pl.TrackId equals t.Id
// where pl.PlayListId == playlist.Id
// select t.Duration).Sum()
// where (playlist.IsPublic) || (roadieUser != null && playlist.UserId == roadieUser.Id)
// select new subsonic.Playlist
// {
// id = playlist.RoadieId.ToString(),
// name = playlist.Name,
// comment = playlist.Description,
// owner = u.UserName,
// songCount = trackCount,
// duration = playListDuration.ToSecondsFromMilliseconds(),
// created = playlist.CreatedDate,
// changed = playlist.LastUpdated ?? playlist.CreatedDate,
// coverArt = this.MakePlaylistThumbnailImage(playlist.RoadieId).Url,
// @public = playlist.IsPublic,
// publicSpecified = playlist.IsPublic
// }
// );
var pagedRequest = request.PagedRequest;
pagedRequest.Sort = "Playlist.Text";
pagedRequest.Order = "ASC";
var playlistResult = await this.PlaylistService.List(pagedRequest, roadieUser);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
@ -869,7 +877,7 @@ namespace Roadie.Api.Services
ItemElementName = subsonic.ItemChoiceType.playlists,
Item = new subsonic.Playlists
{
playlist = playlists.ToArray()
playlist = this.SubsonicPlaylistsForPlaylists(playlistResult.Rows)
}
}
};
@ -1518,12 +1526,21 @@ namespace Roadie.Api.Services
return tracks.Select(x => this.SubsonicChildForTrack(x)).ToArray();
}
private subsonic.Playlist SubsonicPlaylistForPlaylist(Library.Models.Playlists.PlaylistList playlist, IEnumerable<TrackList> playlistTracks)
private subsonic.Playlist[] SubsonicPlaylistsForPlaylists(IEnumerable<Library.Models.Playlists.PlaylistList> playlists)
{
if (playlists == null || !playlists.Any())
{
return new subsonic.Playlist[0];
}
return playlists.Select(x => this.SubsonicPlaylistForPlaylist(x)).ToArray();
}
private subsonic.Playlist SubsonicPlaylistForPlaylist(Library.Models.Playlists.PlaylistList playlist, IEnumerable<TrackList> playlistTracks = null)
{
return new subsonic.PlaylistWithSongs
{
coverArt = this.MakePlaylistThumbnailImage(playlist.Id).Url,
allowedUser = this.AllowedUsers(),
allowedUser = playlist.IsPublic ? this.AllowedUsers() : null,
changed = playlist.LastUpdated ?? playlist.CreatedDate ?? DateTime.UtcNow,
created = playlist.CreatedDate ?? DateTime.UtcNow,
duration = playlist.Duration ?? 0,

View file

@ -239,6 +239,7 @@ namespace Roadie.Api.Services
join p in this.DbContext.Playlists on plt.PlayListId equals p.Id
join t in this.DbContext.Tracks on plt.TrackId equals t.Id
where p.RoadieId == request.FilterToPlaylistId.Value
orderby plt.ListNumber
select new {
plt.ListNumber, t.Id
}).Skip(request.SkipValue).Take(request.LimitValue).ToDictionary(x => x.Id, x => x.ListNumber);
@ -467,6 +468,7 @@ namespace Roadie.Api.Services
info.Bytes = trackBytes;
return new OperationResult<TrackStreamInfo>
{
IsSuccess = true,
Data = info
};
}

View file

@ -7,7 +7,7 @@
}
},
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.RollingFileAlternate", "Serilog.Sinks.SQLite" ],
"MinimumLevel": {
"Default": "Verbose",
"Override": {
@ -20,17 +20,24 @@
"Name": "Console",
"Args": {
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
},
"Default": "Verbose"
}
},
{
"Name": "File",
"Name": "RollingFileAlternate",
"Args": {
"restrictedToMinimumLevel": "Information",
"rollingInterval": "Day",
"path": "C:\\logs\\roadie-log-.log",
"path": "{Date}.log",
"logDirectory": "logs",
"fileSizeLimitBytes": 26214400,
"buffered": true
}
},
{
"Name": "SQLite",
"Args": {
"restrictedToMinimumLevel": "Error",
"sqliteDbPath": "logs\\errors.sqlite"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId", "WithExceptionDetails" ],

View file

@ -17,10 +17,6 @@
<PackageReference Include="Orthogonal.NTagLite" Version="2.0.9" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="RestSharp" Version="106.5.4" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.0" />
<PackageReference Include="SerilogTraceListener" Version="3.1.0" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0006" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0005" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0005" />