From be7aa45f166b53487d8961913cdffddf8fbccf26 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Sun, 25 Nov 2018 14:43:52 -0600 Subject: [PATCH] Subsonic API Work. --- RoadieApi/Controllers/SubsonicController.cs | 222 +++++----- RoadieApi/Controllers/TrackController.cs | 4 +- RoadieApi/Services/ArtistService.cs | 19 +- RoadieApi/Services/IPlayActivityService.cs | 3 +- RoadieApi/Services/ISubsonicService.cs | 2 + RoadieApi/Services/ITrackService.cs | 2 +- RoadieApi/Services/ImageService.cs | 2 +- RoadieApi/Services/PlayActivityService.cs | 3 +- RoadieApi/Services/SubsonicService.cs | 432 +++++++++++--------- RoadieApi/Services/TrackService.cs | 3 +- RoadieApi/appsettings.json | 9 +- 11 files changed, 378 insertions(+), 323 deletions(-) diff --git a/RoadieApi/Controllers/SubsonicController.cs b/RoadieApi/Controllers/SubsonicController.cs index ca95b4d..91abb1d 100644 --- a/RoadieApi/Controllers/SubsonicController.cs +++ b/RoadieApi/Controllers/SubsonicController.cs @@ -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; } /// @@ -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 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 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 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 UpdatePlaylist(SubsonicRequest request, string playlistId, string name, string comment, bool? @public, string[] songIdToAdd, int[] songIndexToRemove) + public async Task 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 GetBookmarks(SubsonicRequest request) + public async Task 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 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); + 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 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 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 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 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 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 Download(SubsonicRequest request) + public async Task 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 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 AuthenticateUser(SubsonicRequest request) { diff --git a/RoadieApi/Controllers/TrackController.cs b/RoadieApi/Controllers/TrackController.cs index a223baf..5be1ee7 100644 --- a/RoadieApi/Controllers/TrackController.cs +++ b/RoadieApi/Controllers/TrackController.cs @@ -57,8 +57,8 @@ namespace Roadie.Api.Controllers [ProducesResponseType(200)] public async Task 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); diff --git a/RoadieApi/Services/ArtistService.cs b/RoadieApi/Services/ArtistService.cs index 5323781..5cc3393 100644 --- a/RoadieApi/Services/ArtistService.cs +++ b/RoadieApi/Services/ArtistService.cs @@ -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 }; } } diff --git a/RoadieApi/Services/IPlayActivityService.cs b/RoadieApi/Services/IPlayActivityService.cs index 600d102..2eb301d 100644 --- a/RoadieApi/Services/IPlayActivityService.cs +++ b/RoadieApi/Services/IPlayActivityService.cs @@ -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> List(PagedRequest request, User roadieUser = null); + Task> List(PagedRequest request, User roadieUser = null, DateTime? newerThan = null); Task> CreatePlayActivity(User roadieUser, TrackStreamInfo streamInfo); } diff --git a/RoadieApi/Services/ISubsonicService.cs b/RoadieApi/Services/ISubsonicService.cs index 1882b8b..449d270 100644 --- a/RoadieApi/Services/ISubsonicService.cs +++ b/RoadieApi/Services/ISubsonicService.cs @@ -66,6 +66,8 @@ namespace Roadie.Api.Services SubsonicOperationResult Ping(Request request); + Task> GetNowPlaying(Request request, Roadie.Library.Models.Users.User roadieUser); + Task> Search(Request request, Roadie.Library.Models.Users.User roadieUser, SearchVersion version); Task> ToggleStar(Request request, Roadie.Library.Models.Users.User roadieUser, bool star, string[] albumIds = null, string[] artistIds = null); diff --git a/RoadieApi/Services/ITrackService.cs b/RoadieApi/Services/ITrackService.cs index c13a84c..819e38a 100644 --- a/RoadieApi/Services/ITrackService.cs +++ b/RoadieApi/Services/ITrackService.cs @@ -12,7 +12,7 @@ namespace Roadie.Api.Services { Task> ById(User roadieUser, Guid id, IEnumerable includes); - Task> List(User roadieUser, PagedRequest request, bool? doRandomize = false, Guid? releaseId = null); + Task> List(PagedRequest request, User roadieUser, bool? doRandomize = false, Guid? releaseId = null); Task> TrackStreamInfo(Guid trackId, long beginBytes, long endBytes); } diff --git a/RoadieApi/Services/ImageService.cs b/RoadieApi/Services/ImageService.cs index 1fb0bb1..071f5cc 100644 --- a/RoadieApi/Services/ImageService.cs +++ b/RoadieApi/Services/ImageService.cs @@ -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(); diff --git a/RoadieApi/Services/PlayActivityService.cs b/RoadieApi/Services/PlayActivityService.cs index 3df86ec..b7f5f36 100644 --- a/RoadieApi/Services/PlayActivityService.cs +++ b/RoadieApi/Services/PlayActivityService.cs @@ -37,7 +37,7 @@ namespace Roadie.Api.Services this.PlayActivityHub = playHubContext; } - public async Task> List(PagedRequest request, User roadieUser = null) + public async Task> 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()) || diff --git a/RoadieApi/Services/SubsonicService.cs b/RoadieApi/Services/SubsonicService.cs index 665a0e4..8ae3633 100644 --- a/RoadieApi/Services/SubsonicService.cs +++ b/RoadieApi/Services/SubsonicService.cs @@ -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 UserManger { get; } @@ -56,6 +57,7 @@ namespace Roadie.Api.Services IReleaseService releaseService, IImageService imageService, IBookmarkService bookmarkService, + IPlayActivityService playActivityService, UserManager 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 }; } + /// + /// Creates (or updates) a playlist. + /// + /// Populated Subsonic Request + /// Populated Roadie User + /// The human-readable name of the playlist. + /// ID of a song in the playlist. Use one songId parameter for each song in the playlist. + /// The playlist ID. (if updating else blank is adding) + /// + public async Task> CreatePlaylist(subsonic.Request request, User roadieUser, string name, string[] songIds, string playlistId = null) + { + data.Playlist playlist = null; + + Guid?[] songRoadieIds = new Guid?[0]; + IQueryable 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.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(); + 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); + } + /// /// Deletes the bookmark for a given file. /// @@ -235,6 +324,43 @@ namespace Roadie.Api.Services }; } + /// + /// Deletes a saved playlist. + /// + public async Task> DeletePlaylist(subsonic.Request request, User roadieUser) + { + if (!request.PlaylistId.HasValue) + { + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.id }]"); + } + var playlist = this.GetPlaylist(request.PlaylistId.Value); + if (playlist == null) + { + return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.TrackId.Value }]"); + } + if (playlist.UserId != roadieUser.Id && !roadieUser.IsAdmin) + { + return new subsonic.SubsonicOperationResult(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 + { + IsSuccess = true, + Data = new subsonic.Response + { + version = SubsonicService.SubsonicVersion, + status = subsonic.ResponseStatus.ok + } + }; + } + /// /// Returns details for an album, including a list of songs. This method organizes music according to ID3 tags. /// @@ -251,7 +377,7 @@ namespace Roadie.Api.Services return new subsonic.SubsonicOperationResult(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 @@ -489,48 +615,11 @@ namespace Roadie.Api.Services /// public async Task> GetArtists(subsonic.Request request, User roadieUser) { - var indexes = new List(); - 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>(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 - { - 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); } /// @@ -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 { 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 { IsSuccess = true, @@ -945,7 +1034,7 @@ namespace Roadie.Api.Services { var songs = new List(); - var randomSongs = await this.TrackService.List(roadieUser, request.PagedRequest, true); + var randomSongs = await this.TrackService.List(request.PagedRequest, roadieUser, true); return new subsonic.SubsonicOperationResult { @@ -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 { @@ -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 { 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.ErrorCodes.TheRequestedDataWasNotFound, $"Unknown Star Id [{ JsonConvert.SerializeObject(request) }]"); } - /// - /// Creates (or updates) a playlist. - /// - /// Populated Subsonic Request - /// Populated Roadie User - /// The human-readable name of the playlist. - /// ID of a song in the playlist. Use one songId parameter for each song in the playlist. - /// The playlist ID. (if updating else blank is adding) - /// - public async Task> CreatePlaylist(subsonic.Request request, User roadieUser, string name, string[] songIds, string playlistId = null) - { - data.Playlist playlist = null; - - Guid?[] songRoadieIds = new Guid?[0]; - IQueryable 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.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(); - 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); - } - - /// - /// Deletes a saved playlist. - /// - public async Task> DeletePlaylist(subsonic.Request request, User roadieUser) - { - if (!request.PlaylistId.HasValue) - { - return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.id }]"); - } - var playlist = this.GetPlaylist(request.PlaylistId.Value); - if (playlist == null) - { - return new subsonic.SubsonicOperationResult(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.TrackId.Value }]"); - } - if(playlist.UserId != roadieUser.Id && !roadieUser.IsAdmin) - { - return new subsonic.SubsonicOperationResult(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 - { - IsSuccess = true, - Data = new subsonic.Response - { - version = SubsonicService.SubsonicVersion, - status = subsonic.ResponseStatus.ok - } - }; - } - /// /// Updates a playlist. Only the owner of a playlist is allowed to update it. /// @@ -1565,7 +1531,7 @@ namespace Roadie.Api.Services /// true if the playlist should be visible to all users, false otherwise. /// Add this song with this ID to the playlist. Multiple parameters allowed /// Remove the song at this position in the playlist. Multiple parameters allowed. - public async Task> 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> 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 }; } + /// + /// Returns what is currently being played by all users. Takes no extra parameters. + /// + public async Task> 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(); + 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(); + 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 + { + 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> GetArtistsAction(subsonic.Request request, User roadieUser) + { + var indexes = new List(); + 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 + { + 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(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) }); }; } diff --git a/RoadieApi/Services/TrackService.cs b/RoadieApi/Services/TrackService.cs index e80c006..b626dc0 100644 --- a/RoadieApi/Services/TrackService.cs +++ b/RoadieApi/Services/TrackService.cs @@ -217,7 +217,7 @@ namespace Roadie.Api.Services return result; } - public async Task> List(User roadieUser, PagedRequest request, bool? doRandomize = false, Guid? releaseId = null) + public async Task> 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) diff --git a/RoadieApi/appsettings.json b/RoadieApi/appsettings.json index c2349b6..60568ef 100644 --- a/RoadieApi/appsettings.json +++ b/RoadieApi/appsettings.json @@ -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,