Subsonic API Work.

This commit is contained in:
Steven Hildreth 2018-11-25 14:43:52 -06:00
parent 4a62537a42
commit be7aa45f16
11 changed files with 378 additions and 323 deletions

View file

@ -12,7 +12,6 @@ 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;
@ -24,6 +23,7 @@ namespace Roadie.Api.Controllers
public class SubsonicController : EntityControllerBase
{
private IPlayActivityService PlayActivityService { get; }
private IReleaseService ReleaseService { get; }
private ISubsonicService SubsonicService { get; }
/// <summary>
@ -32,7 +32,6 @@ namespace Roadie.Api.Controllers
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<ApplicationUser> userManager)
: base(cacheManager, configuration, userManager)
@ -44,20 +43,6 @@ namespace Roadie.Api.Controllers
this.PlayActivityService = playActivityService;
}
[HttpGet("getAlbum.view")]
[HttpPost("getAlbum.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> 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)]
@ -72,6 +57,19 @@ namespace Roadie.Api.Controllers
return this.BuildResponse(request, result);
}
[HttpGet("createPlaylist.view")]
[HttpPost("createPlaylist.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> 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("deleteBookmark.view")]
[HttpPost("deleteBookmark.view")]
@ -101,75 +99,46 @@ namespace Roadie.Api.Controllers
return this.BuildResponse(request, result);
}
[HttpGet("updatePlaylist.view")]
[HttpPost("updatePlaylist.view")]
[HttpGet("download.view")]
[HttpPost("download.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> UpdatePlaylist(SubsonicRequest request, string playlistId, string name, string comment, bool? @public, string[] songIdToAdd, int[] songIndexToRemove)
public async Task<IActionResult> Download(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
return Unauthorized();
}
var result = await this.SubsonicService.UpdatePlaylist(request, this.SubsonicUser, playlistId, name, comment, @public, songIdToAdd, songIndexToRemove);
return this.BuildResponse(request, result);
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 }`");
}
[HttpGet("getBookmarks.view")]
[HttpPost("getBookmarks.view")]
[HttpGet("getAlbum.view")]
[HttpPost("getAlbum.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> GetBookmarks(SubsonicRequest request)
public async Task<IActionResult> GetAlbum(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<IActionResult> 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<IActionResult> 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<IActionResult> 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);
var result = await this.SubsonicService.GetAlbum(request, this.SubsonicUser);
return this.BuildResponse(request, result, "album");
}
[HttpGet("getAlbumInfo.view")]
@ -293,6 +262,20 @@ namespace Roadie.Api.Controllers
return Redirect($"/images/user/{ user.RoadieId }/{this.RoadieSettings.ThumbnailImageSize.Width}/{this.RoadieSettings.ThumbnailImageSize.Height}");
}
[HttpGet("getBookmarks.view")]
[HttpPost("getBookmarks.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> 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("getCoverArt.view")]
[HttpPost("getCoverArt.view")]
[ProducesResponseType(200)]
@ -379,6 +362,20 @@ namespace Roadie.Api.Controllers
return this.BuildResponse(request, result, "musicFolders");
}
[HttpGet("getNowPlaying.view")]
[HttpPost("getNowPlaying.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> GetNowPlaying(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetNowPlaying(request, this.SubsonicUser);
return this.BuildResponse(request, result, "nowPlaying");
}
[HttpGet("getPlaylist.view")]
[HttpPost("getPlaylist.view")]
[ProducesResponseType(200)]
@ -393,21 +390,6 @@ namespace Roadie.Api.Controllers
return this.BuildResponse(request, result, "playlist");
}
[HttpGet("createPlaylist.view")]
[HttpPost("createPlaylist.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> 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)]
@ -634,6 +616,34 @@ namespace Roadie.Api.Controllers
return this.BuildResponse(request, result, "searchResult3");
}
[HttpGet("setRating.view")]
[HttpPost("setRating.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> 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("star.view")]
[HttpPost("star.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> 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("stream.view")]
[HttpPost("stream.view")]
[ProducesResponseType(200)]
@ -648,41 +658,37 @@ namespace Roadie.Api.Controllers
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")]
[HttpGet("unstar.view")]
[HttpPost("unstar.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> Download(SubsonicRequest request)
public async Task<IActionResult> UnStar(SubsonicRequest request, string[] albumId, string[] artistId)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return Unauthorized();
return authResult;
}
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 }`");
var result = await this.SubsonicService.ToggleStar(request, this.SubsonicUser, false, albumId, artistId);
return this.BuildResponse(request, result);
}
[HttpGet("updatePlaylist.view")]
[HttpPost("updatePlaylist.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> 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);
}
private async Task<IActionResult> AuthenticateUser(SubsonicRequest request)
{

View file

@ -57,8 +57,8 @@ namespace Roadie.Api.Controllers
[ProducesResponseType(200)]
public async Task<IActionResult> List([FromQuery]PagedRequest request, string inc)
{
var result = await this.TrackService.List(roadieUser: await this.CurrentUserModel(),
request: request);
var result = await this.TrackService.List(request: request,
roadieUser: await this.CurrentUserModel());
if (!result.IsSuccess)
{
return StatusCode((int)HttpStatusCode.InternalServerError);

View file

@ -370,23 +370,23 @@ namespace Roadie.Api.Services
}
if(rows.Any() && (doArtistCounts ?? true))
{
var rowArtistIds = rows.Select(x => x.DatabaseId);
var rowArtistIds = rows.Select(x => x.DatabaseId).ToArray();
var artistReleases = (from a in this.DbContext.Artists
join r in this.DbContext.Releases on a.Id equals r.ArtistId
where a.ReleaseCount > 0
where r.TrackCount > 0
where rowArtistIds.Contains(a.Id)
select new
{
r.Id,
artistId = a.Id,
releaseId = r.Id,
r.TrackCount,
r.PlayedCount
}).ToList();
}).ToArray();
foreach(var row in rows)
{
row.ArtistReleaseCount = artistReleases.Where(r => r.Id == row.DatabaseId).Select(r => r.Id).Count();
row.ArtistTrackCount = artistReleases.Where(r => r.Id == row.DatabaseId).Sum(r => r.TrackCount);
row.ArtistPlayedCount = artistReleases.Where(r => r.Id == row.DatabaseId).Sum(r => r.PlayedCount);
var rowArtistReleases = artistReleases.Where(r => r.artistId == row.DatabaseId);
row.ArtistReleaseCount = rowArtistReleases.Select(r => r.releaseId).Count();
row.ArtistTrackCount = rowArtistReleases.Sum(r => r.TrackCount);
row.ArtistPlayedCount = rowArtistReleases.Sum(r => r.PlayedCount);
}
}
if (rows.Any() && roadieUser != null)
@ -400,7 +400,8 @@ namespace Roadie.Api.Services
{
IsDisliked = userArtistRating.IsDisliked ?? false,
IsFavorite = userArtistRating.IsFavorite ?? false,
Rating = userArtistRating.Rating
Rating = userArtistRating.Rating,
RatedDate = userArtistRating.LastUpdated ?? userArtistRating.CreatedDate
};
}
}

View file

@ -2,13 +2,14 @@
using Roadie.Library.Models;
using Roadie.Library.Models.Pagination;
using Roadie.Library.Models.Users;
using System;
using System.Threading.Tasks;
namespace Roadie.Api.Services
{
public interface IPlayActivityService
{
Task<PagedResult<PlayActivityList>> List(PagedRequest request, User roadieUser = null);
Task<PagedResult<PlayActivityList>> List(PagedRequest request, User roadieUser = null, DateTime? newerThan = null);
Task<OperationResult<PlayActivityList>> CreatePlayActivity(User roadieUser, TrackStreamInfo streamInfo);
}

View file

@ -66,6 +66,8 @@ namespace Roadie.Api.Services
SubsonicOperationResult<Response> Ping(Request request);
Task<SubsonicOperationResult<Response>> GetNowPlaying(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<SubsonicOperationResult<Response>> Search(Request request, Roadie.Library.Models.Users.User roadieUser, SearchVersion version);
Task<SubsonicOperationResult<Response>> ToggleStar(Request request, Roadie.Library.Models.Users.User roadieUser, bool star, string[] albumIds = null, string[] artistIds = null);

View file

@ -12,7 +12,7 @@ namespace Roadie.Api.Services
{
Task<OperationResult<Track>> ById(User roadieUser, Guid id, IEnumerable<string> includes);
Task<Library.Models.Pagination.PagedResult<TrackList>> List(User roadieUser, PagedRequest request, bool? doRandomize = false, Guid? releaseId = null);
Task<Library.Models.Pagination.PagedResult<TrackList>> List(PagedRequest request, User roadieUser, bool? doRandomize = false, Guid? releaseId = null);
Task<OperationResult<TrackStreamInfo>> TrackStreamInfo(Guid trackId, long beginBytes, long endBytes);
}

View file

@ -328,7 +328,7 @@ namespace Roadie.Api.Services
result.LastModified = DateTime.UtcNow;
if (width.Value != this.Configuration.ThumbnailImageSize.Width || height.Value != this.Configuration.ThumbnailImageSize.Height)
{
this.Logger.LogInformation($"{ type }: Resized [{ id }], Width [{ width.Value }], Height [{ height.Value }]");
this.Logger.LogTrace($"{ type }: Resized [{ id }], Width [{ width.Value }], Height [{ height.Value }]");
}
}
sw.Stop();

View file

@ -37,7 +37,7 @@ namespace Roadie.Api.Services
this.PlayActivityHub = playHubContext;
}
public async Task<Library.Models.Pagination.PagedResult<PlayActivityList>> List(PagedRequest request, User roadieUser = null)
public async Task<Library.Models.Pagination.PagedResult<PlayActivityList>> List(PagedRequest request, User roadieUser = null, DateTime? newerThan = null)
{
try
{
@ -52,6 +52,7 @@ namespace Roadie.Api.Services
join usertrack in this.DbContext.UserTracks on t.Id equals usertrack.TrackId
join u in this.DbContext.Users on usertrack.UserId equals u.Id
join releaseArtist in this.DbContext.Artists on r.ArtistId equals releaseArtist.Id
where(newerThan == null || usertrack.LastPlayed >= newerThan)
where ((roadieUser == null && !(u.IsPrivate ?? false)) || (roadieUser != null && (usertrack != null && usertrack.User.Id == roadieUser.Id)))
where (request.FilterValue.Length == 0 || (request.FilterValue.Length > 0 && (
t.Title != null && t.Title.ToLower().Contains(request.Filter.ToLower()) ||

View file

@ -39,6 +39,7 @@ namespace Roadie.Api.Services
private ICollectionService CollectionService { get; }
private IImageService ImageService { get; }
private IPlaylistService PlaylistService { get; }
private IPlayActivityService PlayActivityService { get; }
private IReleaseService ReleaseService { get; }
private ITrackService TrackService { get; }
private UserManager<ApplicationUser> UserManger { get; }
@ -56,6 +57,7 @@ namespace Roadie.Api.Services
IReleaseService releaseService,
IImageService imageService,
IBookmarkService bookmarkService,
IPlayActivityService playActivityService,
UserManager<ApplicationUser> userManager
)
: base(configuration, httpEncoder, context, cacheManager, logger, httpContext)
@ -65,6 +67,7 @@ namespace Roadie.Api.Services
this.CollectionService = collectionService;
this.ImageService = imageService;
this.PlaylistService = playlistService;
this.PlayActivityService = playActivityService;
this.ReleaseService = releaseService;
this.TrackService = trackService;
this.UserManger = userManager;
@ -199,6 +202,92 @@ namespace Roadie.Api.Services
};
}
/// <summary>
/// Creates (or updates) a playlist.
/// </summary>
/// <param name="request">Populated Subsonic Request</param>
/// <param name="roadieUser">Populated Roadie User</param>
/// <param name="name">The human-readable name of the playlist.</param>
/// <param name="songIds">ID of a song in the playlist. Use one songId parameter for each song in the playlist.</param>
/// <param name="playlistId">The playlist ID. (if updating else blank is adding)</param>
/// <returns></returns>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> CreatePlaylist(subsonic.Request request, User roadieUser, string name, string[] songIds, string playlistId = null)
{
data.Playlist playlist = null;
Guid?[] songRoadieIds = new Guid?[0];
IQueryable<data.Track> submittedTracks = new data.Track[0].AsQueryable();
if (songIds != null && songIds.Any())
{
songRoadieIds = songIds.Select(x => SafeParser.ToGuid(x)).ToArray();
// Add (if not already) given tracks to Playlist
submittedTracks = (from t in this.DbContext.Tracks
where songRoadieIds.Contains(t.RoadieId)
select t);
}
var didCreate = false;
if (!string.IsNullOrEmpty(playlistId))
{
request.id = playlistId;
playlist = this.DbContext.Playlists.Include(x => x.Tracks).FirstOrDefault(x => x.RoadieId == request.PlaylistId);
if (playlist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid PlaylistId [{ playlistId }]");
}
// When Create is called again on an existing delete all existing tracks and add given
if (playlist.Tracks != null && playlist.Tracks.Any())
{
this.DbContext.PlaylistTracks.RemoveRange(playlist.Tracks);
}
var listNumber = playlist.Tracks != null && playlist.Tracks.Any() ? playlist.Tracks?.Max(x => x.ListNumber) ?? 0 : 0;
foreach (var submittedTrack in submittedTracks)
{
if (playlist.Tracks == null || !playlist.Tracks.Any(x => x.TrackId == submittedTrack.Id))
{
listNumber++;
this.DbContext.PlaylistTracks.Add(new data.PlaylistTrack
{
PlayListId = playlist.Id,
ListNumber = listNumber,
TrackId = submittedTrack.Id
});
}
}
playlist.Name = name ?? playlist.Name;
playlist.LastUpdated = DateTime.UtcNow;
}
else
{
var tracks = new List<data.PlaylistTrack>();
var listNumber = 0;
foreach (var submittedTrack in submittedTracks)
{
listNumber++;
tracks.Add(new data.PlaylistTrack
{
PlayListId = playlist.Id,
ListNumber = listNumber,
TrackId = submittedTrack.Id
});
}
playlist = new data.Playlist
{
IsPublic = false,
Name = name,
UserId = roadieUser.Id,
Tracks = tracks
};
didCreate = true;
this.DbContext.Playlists.Add(playlist);
}
await this.DbContext.SaveChangesAsync();
this.Logger.LogInformation($"Subsonic: User `{ roadieUser }` { (didCreate ? "created" : "modified") } Playlist `{ playlist }` added [{ songRoadieIds.Count() }] Tracks.");
request.id = subsonic.Request.PlaylistdIdentifier + playlist.RoadieId.ToString();
return await this.GetPlaylist(request, roadieUser);
}
/// <summary>
/// Deletes the bookmark for a given file.
/// </summary>
@ -235,6 +324,43 @@ namespace Roadie.Api.Services
};
}
/// <summary>
/// Deletes a saved playlist.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> DeletePlaylist(subsonic.Request request, User roadieUser)
{
if (!request.PlaylistId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.id }]");
}
var playlist = this.GetPlaylist(request.PlaylistId.Value);
if (playlist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.TrackId.Value }]");
}
if (playlist.UserId != roadieUser.Id && !roadieUser.IsAdmin)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.UserIsNotAuthorizedForGivenOperation, "User is not allowed to delete playlist.");
}
this.DbContext.Playlists.Remove(playlist);
await this.DbContext.SaveChangesAsync();
var user = this.GetUser(roadieUser.UserId);
this.CacheManager.ClearRegion(user.CacheRegion);
this.Logger.LogInformation($"Subsonic: Deleted Playlist `{ playlist}` for User `{ roadieUser }`");
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok
}
};
}
/// <summary>
/// Returns details for an album, including a list of songs. This method organizes music according to ID3 tags.
/// </summary>
@ -251,7 +377,7 @@ namespace Roadie.Api.Services
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, releaseId);
var releaseTracks = await this.TrackService.List(pagedRequest, roadieUser, false, 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 subsonic.SubsonicOperationResult<subsonic.Response>
@ -489,48 +615,11 @@ namespace Roadie.Api.Services
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetArtists(subsonic.Request request, User roadieUser)
{
var indexes = new List<subsonic.IndexID3>();
var musicFolder = this.MusicFolders().FirstOrDefault(x => x.id == (request.MusicFolderId ?? 2));
var pagedRequest = request.PagedRequest;
if (musicFolder == this.CollectionMusicFolder())
var cacheKey = $"urn:subsonic_artists:{ roadieUser.UserName }";
return await this.CacheManager.GetAsync<subsonic.SubsonicOperationResult<subsonic.Response>>(cacheKey, async () =>
{
// Indexes for "Collection" Artists alphabetically
// not sure what to do here since this is albums not artists in a "Collection".
}
else
{
// Indexes for "Music" Artists alphabetically
pagedRequest.SkipValue = 0;
pagedRequest.Limit = int.MaxValue;
pagedRequest.Sort = "Artist.Text";
var artistList = await this.ArtistService.List(roadieUser: roadieUser,
request:pagedRequest,
doRandomize: false,
onlyIncludeWithReleases: true,
doArtistCounts: false);
foreach (var artistGroup in artistList.Rows.GroupBy(x => x.Artist.Text.Substring(0, 1)))
{
indexes.Add(new subsonic.IndexID3
{
name = artistGroup.Key,
artist = this.SubsonicArtistID3sForArtists(artistGroup)
});
};
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.artists,
Item = new subsonic.ArtistsID3
{
index = indexes.ToArray()
}
}
};
return await this.GetArtistsAction(request, roadieUser);
}, CacheManagerBase.SystemCacheRegionUrn);
}
/// <summary>
@ -543,7 +632,7 @@ namespace Roadie.Api.Services
pagedRequest.Order = "DESC";
var userBookmarkResult = await this.BookmarkService.List(roadieUser, pagedRequest, false, Library.Enums.BookmarkType.Track);
pagedRequest.FilterToTrackIds = userBookmarkResult.Rows.Select(x => SafeParser.ToGuid(x.Bookmark.Value)).ToArray();
var trackListResult = await this.TrackService.List(roadieUser, pagedRequest);
var trackListResult = await this.TrackService.List(pagedRequest, roadieUser);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
@ -814,7 +903,7 @@ namespace Roadie.Api.Services
directory.starredSpecified = true;
}
var pagedRequest = request.PagedRequest;
var songTracks = await this.TrackService.List(roadieUser, pagedRequest, false, release.RoadieId);
var songTracks = await this.TrackService.List(pagedRequest, roadieUser, false, release.RoadieId);
directory.child = this.SubsonicChildrenForTracks(songTracks.Rows);
directory.playCount = directory.child.Select(x => x.playCount).Sum();
}
@ -877,7 +966,7 @@ namespace Roadie.Api.Services
}
// 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);
var tracksForPlaylist = await this.TrackService.List(pagedRequest, roadieUser);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
@ -945,7 +1034,7 @@ namespace Roadie.Api.Services
{
var songs = new List<subsonic.Child>();
var randomSongs = await this.TrackService.List(roadieUser, request.PagedRequest, true);
var randomSongs = await this.TrackService.List(request.PagedRequest, roadieUser, true);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
@ -1021,7 +1110,7 @@ namespace Roadie.Api.Services
var pagedRequest = request.PagedRequest;
pagedRequest.FilterToTrackId = request.TrackId.Value;
pagedRequest.Sort = "Id";
var trackResult = await this.TrackService.List(roadieUser, pagedRequest);
var trackResult = await this.TrackService.List(pagedRequest, roadieUser);
var track = trackResult.Rows.Any() ? trackResult.Rows.First() : null;
if (track == null)
{
@ -1048,7 +1137,7 @@ namespace Roadie.Api.Services
var pagedRequest = request.PagedRequest;
pagedRequest.FilterByGenre = request.Genre;
pagedRequest.Sort = "Id";
var trackResult = await this.TrackService.List(roadieUser, pagedRequest);
var trackResult = await this.TrackService.List(pagedRequest, roadieUser);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
@ -1077,7 +1166,7 @@ namespace Roadie.Api.Services
var artistList = await this.ArtistService.List(roadieUser, pagedRequest);
var releaseList = await this.ReleaseService.List(roadieUser, pagedRequest);
var songList = await this.TrackService.List(roadieUser, pagedRequest);
var songList = await this.TrackService.List(pagedRequest, roadieUser);
switch (version)
{
@ -1132,7 +1221,7 @@ namespace Roadie.Api.Services
{
artist = base.GetArtist(request.ArtistName);
}
else if(request.ArtistId.HasValue)
else if (request.ArtistId.HasValue)
{
artist = this.GetArtist(request.ArtistId.Value);
}
@ -1145,7 +1234,7 @@ namespace Roadie.Api.Services
pagedRequest.FilterTopPlayedOnly = true;
pagedRequest.Sort = "PlayedCount";
pagedRequest.Order = "DESC";
var trackResult = await this.TrackService.List(roadieUser, pagedRequest);
var trackResult = await this.TrackService.List(pagedRequest, roadieUser);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
@ -1251,7 +1340,7 @@ namespace Roadie.Api.Services
trackPagedRequest.Limit = request.SongCount ?? trackPagedRequest.Limit;
trackPagedRequest.SkipValue = request.SongOffset ?? trackPagedRequest.SkipValue;
trackPagedRequest.Filter = query;
var songResult = await this.TrackService.List(roadieUser, trackPagedRequest);
var songResult = await this.TrackService.List(trackPagedRequest, roadieUser);
var songs = this.SubsonicChildrenForTracks(songResult.Rows);
switch (version)
@ -1432,129 +1521,6 @@ namespace Roadie.Api.Services
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Unknown Star Id [{ JsonConvert.SerializeObject(request) }]");
}
/// <summary>
/// Creates (or updates) a playlist.
/// </summary>
/// <param name="request">Populated Subsonic Request</param>
/// <param name="roadieUser">Populated Roadie User</param>
/// <param name="name">The human-readable name of the playlist.</param>
/// <param name="songIds">ID of a song in the playlist. Use one songId parameter for each song in the playlist.</param>
/// <param name="playlistId">The playlist ID. (if updating else blank is adding)</param>
/// <returns></returns>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> CreatePlaylist(subsonic.Request request, User roadieUser, string name, string[] songIds, string playlistId = null)
{
data.Playlist playlist = null;
Guid?[] songRoadieIds = new Guid?[0];
IQueryable<data.Track> submittedTracks = new data.Track[0].AsQueryable();
if (songIds != null && songIds.Any())
{
songRoadieIds = songIds.Select(x => SafeParser.ToGuid(x)).ToArray();
// Add (if not already) given tracks to Playlist
submittedTracks = (from t in this.DbContext.Tracks
where songRoadieIds.Contains(t.RoadieId)
select t);
}
var didCreate = false;
if (!string.IsNullOrEmpty(playlistId))
{
request.id = playlistId;
playlist = this.DbContext.Playlists.Include(x => x.Tracks).FirstOrDefault(x => x.RoadieId == request.PlaylistId);
if(playlist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid PlaylistId [{ playlistId }]");
}
// When Create is called again on an existing delete all existing tracks and add given
if (playlist.Tracks != null && playlist.Tracks.Any())
{
this.DbContext.PlaylistTracks.RemoveRange(playlist.Tracks);
}
var listNumber = playlist.Tracks != null && playlist.Tracks.Any() ? playlist.Tracks?.Max(x => x.ListNumber) ?? 0 : 0;
foreach (var submittedTrack in submittedTracks)
{
if (playlist.Tracks == null || !playlist.Tracks.Any(x => x.TrackId == submittedTrack.Id))
{
listNumber++;
this.DbContext.PlaylistTracks.Add(new data.PlaylistTrack
{
PlayListId = playlist.Id,
ListNumber = listNumber,
TrackId = submittedTrack.Id
});
}
}
playlist.Name = name ?? playlist.Name;
playlist.LastUpdated = DateTime.UtcNow;
}
else
{
var tracks = new List<data.PlaylistTrack>();
var listNumber = 0;
foreach (var submittedTrack in submittedTracks)
{
listNumber++;
tracks.Add(new data.PlaylistTrack
{
PlayListId = playlist.Id,
ListNumber = listNumber,
TrackId = submittedTrack.Id
});
}
playlist = new data.Playlist
{
IsPublic = false,
Name = name,
UserId = roadieUser.Id,
Tracks = tracks
};
didCreate = true;
this.DbContext.Playlists.Add(playlist);
}
await this.DbContext.SaveChangesAsync();
this.Logger.LogInformation($"Subsonic: User `{ roadieUser }` { (didCreate ? "created": "modified") } Playlist `{ playlist }` added [{ songRoadieIds.Count() }] Tracks.");
request.id = subsonic.Request.PlaylistdIdentifier + playlist.RoadieId.ToString();
return await this.GetPlaylist(request, roadieUser);
}
/// <summary>
/// Deletes a saved playlist.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> DeletePlaylist(subsonic.Request request, User roadieUser)
{
if (!request.PlaylistId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.id }]");
}
var playlist = this.GetPlaylist(request.PlaylistId.Value);
if (playlist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.TrackId.Value }]");
}
if(playlist.UserId != roadieUser.Id && !roadieUser.IsAdmin)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.UserIsNotAuthorizedForGivenOperation, "User is not allowed to delete playlist.");
}
this.DbContext.Playlists.Remove(playlist);
await this.DbContext.SaveChangesAsync();
var user = this.GetUser(roadieUser.UserId);
this.CacheManager.ClearRegion(user.CacheRegion);
this.Logger.LogInformation($"Subsonic: Deleted Playlist `{ playlist}` for User `{ roadieUser }`");
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok
}
};
}
/// <summary>
/// Updates a playlist. Only the owner of a playlist is allowed to update it.
/// </summary>
@ -1565,7 +1531,7 @@ namespace Roadie.Api.Services
/// <param name="isPublic">true if the playlist should be visible to all users, false otherwise.</param>
/// <param name="songIdsToAdd">Add this song with this ID to the playlist. Multiple parameters allowed</param>
/// <param name="songIndexesToRemove">Remove the song at this position in the playlist. Multiple parameters allowed.</param>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> UpdatePlaylist(subsonic.Request request, User roadieUser, string playListId, string name = null, string comment =null, bool? isPublic = null, string[] songIdsToAdd = null, int[] songIndexesToRemove = null)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> UpdatePlaylist(subsonic.Request request, User roadieUser, string playListId, string name = null, string comment = null, bool? isPublic = null, string[] songIdsToAdd = null, int[] songIndexesToRemove = null)
{
request.id = playListId ?? request.id;
if (!request.PlaylistId.HasValue)
@ -1586,7 +1552,7 @@ namespace Roadie.Api.Services
playlist.IsPublic = isPublic ?? playlist.IsPublic;
playlist.LastUpdated = DateTime.UtcNow;
if(songIdsToAdd != null && songIdsToAdd.Any())
if (songIdsToAdd != null && songIdsToAdd.Any())
{
// Add new if not already on Playlist
var songIdsToAddRoadieIds = songIdsToAdd.Select(x => SafeParser.ToGuid(x)).ToArray();
@ -1609,22 +1575,11 @@ namespace Roadie.Api.Services
}
}
}
if(songIndexesToRemove != null && songIndexesToRemove.Any())
if (songIndexesToRemove != null && songIndexesToRemove.Any())
{
// Remove tracks from playlist
//foreach (var submittedTrack in submittedTracks)
//{
// if (playlist.Tracks == null || !playlist.Tracks.Any(x => x.TrackId == submittedTrack.Id))
// {
// listNumber++;
// this.DbContext.PlaylistTracks.Add(new data.PlaylistTrack
// {
// PlayListId = playlist.Id,
// ListNumber = listNumber,
// TrackId = submittedTrack.Id
// });
// }
//}
// Not clear from API documentation if this is zero based, wait until someone calls it to get values passed.
throw new NotImplementedException($"Request [{ JsonConvert.SerializeObject(request) }]");
}
await this.DbContext.SaveChangesAsync();
@ -1643,9 +1598,98 @@ namespace Roadie.Api.Services
};
}
/// <summary>
/// Returns what is currently being played by all users. Takes no extra parameters.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetNowPlaying(subsonic.Request request, User roadieUser)
{
var pagedRequest = request.PagedRequest;
pagedRequest.Sort = "PlayedDateDateTime";
pagedRequest.Order = "DESC";
var playActivityResult = await this.PlayActivityService.List(pagedRequest, roadieUser, DateTime.UtcNow.AddDays(-1));
pagedRequest.Sort = null;
pagedRequest.Order = null;
pagedRequest.FilterToTrackIds = playActivityResult.Rows.Select(x => SafeParser.ToGuid(x.Track.Value)).Distinct().ToArray();
var playActivityTracksResult = await this.TrackService.List(pagedRequest, roadieUser);
var playEntries = new List<subsonic.NowPlayingEntry>();
var now = DateTime.UtcNow;
foreach(var row in playActivityResult.Rows)
{
var rowTrack = playActivityTracksResult.Rows.FirstOrDefault(x => x.Track.Value == row.Track.Value);
var playEntryTrackChild = this.SubsonicChildForTrack(rowTrack);
var playEntry = playEntryTrackChild.Adapt<subsonic.NowPlayingEntry>();
playEntry.username = row.User.Text;
playEntry.minutesAgo = (int)(now - row.PlayedDateDateTime.Value).TotalMinutes;
playEntry.playerId = 0;
playEntry.playerName = string.Empty;
playEntries.Add(playEntry);
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.nowPlaying,
Item = new subsonic.NowPlaying
{
entry = playEntries.ToArray()
}
}
};
}
#region Privates
private async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetArtistsAction(subsonic.Request request, User roadieUser)
{
var indexes = new List<subsonic.IndexID3>();
var musicFolder = this.MusicFolders().FirstOrDefault(x => x.id == (request.MusicFolderId ?? 2));
var pagedRequest = request.PagedRequest;
if (musicFolder == this.CollectionMusicFolder())
{
// Indexes for "Collection" Artists alphabetically
}
else
{
// Indexes for "Music" Artists alphabetically
pagedRequest.SkipValue = 0;
pagedRequest.Limit = int.MaxValue;
pagedRequest.Sort = "Artist.Text";
var artistList = await this.ArtistService.List(roadieUser: roadieUser,
request: pagedRequest,
doRandomize: false,
onlyIncludeWithReleases: true,
doArtistCounts: true);
foreach (var artistGroup in artistList.Rows.GroupBy(x => x.Artist.Text.Substring(0, 1)))
{
indexes.Add(new subsonic.IndexID3
{
name = artistGroup.Key,
artist = this.SubsonicArtistID3sForArtists(artistGroup)
});
};
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.artists,
Item = new subsonic.ArtistsID3
{
index = indexes.ToArray()
}
}
};
}
private string[] AllowedUsers()
{
return this.CacheManager.Get<string[]>(CacheManagerBase.SystemCacheRegionUrn + ":active_usernames", () =>
@ -1698,9 +1742,9 @@ namespace Roadie.Api.Services
pagedRequest.SkipValue = 0;
pagedRequest.Limit = int.MaxValue;
pagedRequest.Sort = "Artist.Text";
var artistList = await this.ArtistService.List(roadieUser:roadieUser,
var artistList = await this.ArtistService.List(roadieUser: roadieUser,
request: pagedRequest,
doRandomize: false,
doRandomize: false,
onlyIncludeWithReleases: true,
doArtistCounts: false);
foreach (var artistGroup in artistList.Rows.GroupBy(x => x.Artist.Text.Substring(0, 1)))
@ -1708,7 +1752,7 @@ namespace Roadie.Api.Services
indexes.Add(new subsonic.Index
{
name = artistGroup.Key,
artist = this.SubsonicArtistsForArtists(artistGroup)
artist = this.SubsonicArtistsForArtists(artistGroup)
});
};
}

View file

@ -217,7 +217,7 @@ namespace Roadie.Api.Services
return result;
}
public async Task<Library.Models.Pagination.PagedResult<TrackList>> List(User roadieUser, PagedRequest request, bool? doRandomize = false, Guid? releaseId = null)
public async Task<Library.Models.Pagination.PagedResult<TrackList>> List(PagedRequest request, User roadieUser, bool? doRandomize = false, Guid? releaseId = null)
{
var sw = new Stopwatch();
sw.Start();
@ -268,7 +268,6 @@ namespace Roadie.Api.Services
randomLimit = request.LimitValue > randomLimit ? randomLimit : request.LimitValue;
var sql = $"SELECT t.* FROM `track` t WHERE t.Hash IS NOT NULL ORDER BY RAND() LIMIT {randomLimit}";
randomTrackIds = this.DbContext.Tracks.FromSql(sql).Select(x => x.Id).ToArray();
}
Guid?[] filterToTrackIds = null;
if(request.FilterToTrackId.HasValue || request.FilterToTrackIds != null)

View file

@ -9,9 +9,9 @@
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.RollingFileAlternate" ],
"MinimumLevel": {
"Default": "Verbose",
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft": "Information",
"System": "Warning"
}
},
@ -19,13 +19,14 @@
{
"Name": "Console",
"Args": {
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
"restrictedToMinimumLevel": "Verbose"
}
},
{
"Name": "RollingFileAlternate",
"Args": {
"restrictedToMinimumLevel": "Information",
"restrictedToMinimumLevel": "Warning",
"path": "{Date}.log",
"logDirectory": "logs",
"fileSizeLimitBytes": 26214400,