roadie/RoadieApi/Services/SubsonicService.cs
2018-11-25 14:43:52 -06:00

2318 lines
No EOL
108 KiB
C#

using Mapster;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Roadie.Library.Caching;
using Roadie.Library.Configuration;
using Roadie.Library.Encoding;
using Roadie.Library.Extensions;
using Roadie.Library.Identity;
using Roadie.Library.Models;
using Roadie.Library.Models.Releases;
using Roadie.Library.Models.Users;
using Roadie.Library.Utility;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using data = Roadie.Library.Data;
using subsonic = Roadie.Library.Models.ThirdPartyApi.Subsonic;
namespace Roadie.Api.Services
{
/// <summary>
/// Subsonic API emulator for Roadie. Enables Subsonic clients to work with Roadie.
/// <seealso cref="http://www.subsonic.org/pages/inc/api/schema/subsonic-rest-api-1.16.1.xsd">
/// <seealso cref="http://www.subsonic.org/pages/api.jsp#getIndexes"/>
/// <seealso cref="https://www.reddit.com/r/subsonic/comments/7c2n6j/database_table_schema/"/>
/// <!-- Generated the classes from the schema above using 'xsd subsonic-rest-api-1.16.1.xsd /c /f /n:Roadie.Library.Models.Subsonic' from Visual Studio Command Prompt -->
/// </summary>
public class SubsonicService : ServiceBase, ISubsonicService
{
public const string SubsonicVersion = "1.16.1";
private IArtistService ArtistService { get; }
private IBookmarkService BookmarkService { get; }
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; }
public SubsonicService(IRoadieSettings configuration,
IHttpEncoder httpEncoder,
IHttpContext httpContext,
data.IRoadieDbContext context,
ICacheManager cacheManager,
ILogger<SubsonicService> logger,
IArtistService artistService,
ITrackService trackService,
ICollectionService collectionService,
IPlaylistService playlistService,
IReleaseService releaseService,
IImageService imageService,
IBookmarkService bookmarkService,
IPlayActivityService playActivityService,
UserManager<ApplicationUser> userManager
)
: base(configuration, httpEncoder, context, cacheManager, logger, httpContext)
{
this.ArtistService = artistService;
this.BookmarkService = bookmarkService;
this.CollectionService = collectionService;
this.ImageService = imageService;
this.PlaylistService = playlistService;
this.PlayActivityService = playActivityService;
this.ReleaseService = releaseService;
this.TrackService = trackService;
this.UserManger = userManager;
}
/// <summary>
/// Authenticate the given credentials and return the corresponding ApplicationUser
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.SubsonicAuthenticateResponse>> Authenticate(subsonic.Request request)
{
if (request == null || string.IsNullOrEmpty(request?.u))
{
return new subsonic.SubsonicOperationResult<subsonic.SubsonicAuthenticateResponse>(subsonic.ErrorCodes.WrongUsernameOrPassword, $"Unknown Username");
}
try
{
var user = this.GetUser(request.u);
if (user == null)
{
this.Logger.LogInformation($"Unknown User [{ request.u }]");
return new subsonic.SubsonicOperationResult<subsonic.SubsonicAuthenticateResponse>(subsonic.ErrorCodes.WrongUsernameOrPassword, $"Unknown Username");
}
var password = request.Password;
var wasAuthenticatedAgainstPassword = false;
if (!string.IsNullOrEmpty(request.s))
{
var token = HashHelper.MD5Hash((user.ApiToken ?? user.Email) + request.s);
if (!token.Equals(request.t, StringComparison.OrdinalIgnoreCase))
{
user = null;
}
else
{
wasAuthenticatedAgainstPassword = true;
}
}
else if (user != null && !string.IsNullOrEmpty(user.PasswordHash) && !string.IsNullOrEmpty(password))
{
try
{
var hashCheck = this.UserManger.PasswordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
if (hashCheck == PasswordVerificationResult.Failed)
{
user = null;
}
else
{
wasAuthenticatedAgainstPassword = true;
}
}
catch
{
}
}
if (wasAuthenticatedAgainstPassword)
{
// Since API dont update LastLogin which likely invalidates any browser logins
user.LastApiAccess = DateTime.UtcNow;
await this.DbContext.SaveChangesAsync();
}
if (user == null)
{
this.Logger.LogInformation($"Unknown User [{ request.u }]");
return new subsonic.SubsonicOperationResult<subsonic.SubsonicAuthenticateResponse>(subsonic.ErrorCodes.WrongUsernameOrPassword, $"Unknown Username");
}
this.Logger.LogInformation($"Subsonic: Successfully Authenticated User [{ user.ToString() }] via Application [{ request.c }], Application Version [{ request.v }]");
return new subsonic.SubsonicOperationResult<subsonic.SubsonicAuthenticateResponse>
{
IsSuccess = true,
Data = new subsonic.SubsonicAuthenticateResponse
{
User = user
}
};
}
catch (Exception ex)
{
this.Logger.LogError(ex, "Subsonic.Authenticate, Error CheckPassword [" + JsonConvert.SerializeObject(request) + "]");
}
return null;
}
/// <summary>
/// Creates or updates a bookmark (a position within a media file). Bookmarks are personal and not visible to other users.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> CreateBookmark(subsonic.Request request, User roadieUser, int position, string comment)
{
if (!request.TrackId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track Id [{ request.id }]");
}
var track = this.GetTrack(request.TrackId.Value);
if (track == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track Id [{ request.TrackId.Value }]");
}
var userBookmark = this.DbContext.Bookmarks.FirstOrDefault(x => x.UserId == roadieUser.Id && x.BookmarkTargetId == track.Id && x.BookmarkType == Library.Enums.BookmarkType.Track);
var createdBookmark = false;
if (userBookmark == null)
{
userBookmark = new data.Bookmark
{
BookmarkTargetId = track.Id,
BookmarkType = Library.Enums.BookmarkType.Track,
UserId = roadieUser.Id,
Comment = comment,
Position = position
};
this.DbContext.Bookmarks.Add(userBookmark);
createdBookmark = true;
}
else
{
userBookmark.LastUpdated = DateTime.UtcNow;
userBookmark.Position = position;
userBookmark.Comment = comment;
}
await this.DbContext.SaveChangesAsync();
var user = this.GetUser(roadieUser.UserId);
this.CacheManager.ClearRegion(user.CacheRegion);
this.Logger.LogInformation($"{ (createdBookmark ? "Created" : "Updated") } Bookmark `{ userBookmark}` for User `{ roadieUser }`");
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok
}
};
}
/// <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>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> DeleteBookmark(subsonic.Request request, User roadieUser)
{
if (!request.TrackId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track Id [{ request.id }]");
}
var track = this.GetTrack(request.TrackId.Value);
if (track == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track Id [{ request.TrackId.Value }]");
}
var userBookmark = this.DbContext.Bookmarks.FirstOrDefault(x => x.UserId == roadieUser.Id && x.BookmarkTargetId == track.Id && x.BookmarkType == Library.Enums.BookmarkType.Track);
if (userBookmark != null)
{
this.DbContext.Bookmarks.Remove(userBookmark);
await this.DbContext.SaveChangesAsync();
var user = this.GetUser(roadieUser.UserId);
this.CacheManager.ClearRegion(user.CacheRegion);
this.Logger.LogInformation($"Subsonic: Deleted Bookmark `{ userBookmark}` for User `{ roadieUser }`");
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok
}
};
}
/// <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>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetAlbum(subsonic.Request request, User roadieUser)
{
var releaseId = SafeParser.ToGuid(request.id);
if (!releaseId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.ReleaseId}]");
}
var release = this.GetRelease(releaseId.Value);
if (release == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.ReleaseId}]");
}
var pagedRequest = request.PagedRequest;
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>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.album,
Item = new subsonic.AlbumWithSongsID3
{
artist = release.Artist.Name,
artistId = subsonic.Request.ArtistIdIdentifier + release.Artist.RoadieId.ToString(),
coverArt = subsonic.Request.ReleaseIdIdentifier + release.RoadieId.ToString(),
created = release.CreatedDate,
duration = release.Duration.ToSecondsFromMilliseconds(),
genre = genre == null ? null : genre.Genre.Name,
id = subsonic.Request.ReleaseIdIdentifier + release.RoadieId.ToString(),
name = release.Title,
playCount = releaseTracks.Rows.Sum(x => x.PlayedCount) ?? 0,
playCountSpecified = releaseTracks.Rows.Any(),
songCount = releaseTracks.Rows.Count(),
starred = userRelease?.LastUpdated ?? userRelease?.CreatedDate ?? DateTime.UtcNow,
starredSpecified = userRelease?.IsFavorite ?? false,
year = release.ReleaseDate != null ? release.ReleaseDate.Value.Year : 0,
yearSpecified = release.ReleaseDate != null,
song = this.SubsonicChildrenForTracks(releaseTracks.Rows)
}
}
};
}
/// <summary>
/// Returns album notes, image URLs etc, using data from last.fm.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetAlbumInfo(subsonic.Request request, User roadieUser, subsonic.AlbumInfoVersion version)
{
var releaseId = SafeParser.ToGuid(request.id);
if (!releaseId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]");
}
var release = this.GetRelease(releaseId.Value);
if (release == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]");
}
switch (version)
{
case subsonic.AlbumInfoVersion.One:
case subsonic.AlbumInfoVersion.Two:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.albumInfo,
Item = new subsonic.AlbumInfo
{
largeImageUrl = this.MakeImage(release.RoadieId, "release", this.Configuration.LargeImageSize).Url,
mediumImageUrl = this.MakeImage(release.RoadieId, "release", this.Configuration.MediumImageSize).Url,
smallImageUrl = this.MakeImage(release.RoadieId, "release", this.Configuration.SmallImageSize).Url,
lastFmUrl = this.MakeLastFmUrl(release.Artist.Name, release.Title),
musicBrainzId = release.MusicBrainzId,
notes = release.Profile
}
}
};
default:
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown Album Info Version [{ request.Type}]");
}
}
/// <summary>
/// Returns a list of random, newest, highest rated etc. albums. Similar to the album lists on the home page of the Subsonic web interface.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetAlbumList(subsonic.Request request, User roadieUser, subsonic.AlbumListVersions version)
{
var releaseResult = new Library.Models.Pagination.PagedResult<ReleaseList>();
switch (request.Type)
{
case subsonic.ListType.Random:
releaseResult = await this.ReleaseService.List(roadieUser, request.PagedRequest, true);
break;
case subsonic.ListType.Highest:
case subsonic.ListType.Recent:
case subsonic.ListType.Newest:
case subsonic.ListType.Frequent:
case subsonic.ListType.AlphabeticalByName:
case subsonic.ListType.AlphabeticalByArtist:
case subsonic.ListType.Starred:
case subsonic.ListType.ByGenre:
case subsonic.ListType.ByYear:
releaseResult = await this.ReleaseService.List(roadieUser, request.PagedRequest);
break;
default:
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown Album List Type [{ request.Type}]");
}
if (!releaseResult.IsSuccess)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(releaseResult.Message);
}
switch (version)
{
case subsonic.AlbumListVersions.One:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.albumList,
Item = new subsonic.AlbumList
{
album = this.SubsonicChildrenForReleases(releaseResult.Rows, null)
}
}
};
case subsonic.AlbumListVersions.Two:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.albumList2,
Item = new subsonic.AlbumList2
{
album = this.SubsonicAlbumID3ForReleases(releaseResult.Rows)
}
}
};
default:
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown AlbumListVersions [{ version }]");
}
}
/// <summary>
/// Returns details for an artist, including a list of albums. This method organizes music according to ID3 tags.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetArtist(subsonic.Request request, User roadieUser)
{
var artistId = SafeParser.ToGuid(request.id);
if (!artistId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]");
}
var pagedRequest = request.PagedRequest;
pagedRequest.Sort = "Id";
pagedRequest.FilterToArtistId = artistId.Value;
var artistResult = await this.ArtistService.List(roadieUser, pagedRequest);
var artist = artistResult.Rows.Any() ? artistResult.Rows.First() : null;
if (artist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release [{ request.id }]");
}
var artistReleaseResult = await this.ReleaseService.List(roadieUser, pagedRequest);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.artist,
Item = this.SubsonicArtistWithAlbumsID3ForArtist(artist, this.SubsonicAlbumID3ForReleases(artistReleaseResult.Rows))
}
};
}
/// <summary>
/// Returns artist info with biography, image URLs and similar artists, using data from last.fm.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetArtistInfo(subsonic.Request request, int? count, bool includeNotPresent, subsonic.ArtistInfoVersion version)
{
var artistId = SafeParser.ToGuid(request.id);
if (!artistId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid ArtistId [{ request.id }]");
}
var artist = this.GetArtist(artistId.Value);
if (artist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid ArtistId [{ request.id }]");
}
switch (version)
{
case subsonic.ArtistInfoVersion.One:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.artistInfo,
Item = this.SubsonicArtistInfoForArtist(artist)
}
};
case subsonic.ArtistInfoVersion.Two:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.artistInfo2,
Item = this.SubsonicArtistInfo2InfoForArtist(artist)
}
};
default:
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown ArtistInfoVersion [{ version }]");
}
}
/// <summary>
/// Similar to getIndexes, but organizes music according to ID3 tags.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetArtists(subsonic.Request request, User roadieUser)
{
var cacheKey = $"urn:subsonic_artists:{ roadieUser.UserName }";
return await this.CacheManager.GetAsync<subsonic.SubsonicOperationResult<subsonic.Response>>(cacheKey, async () =>
{
return await this.GetArtistsAction(request, roadieUser);
}, CacheManagerBase.SystemCacheRegionUrn);
}
/// <summary>
/// Returns all bookmarks for this user. A bookmark is a position within a certain media file.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetBookmarks(subsonic.Request request, User roadieUser)
{
var pagedRequest = request.PagedRequest;
pagedRequest.Sort = "LastUpdated";
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(pagedRequest, roadieUser);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.bookmarks,
Item = new subsonic.Bookmarks
{
bookmark = this.SubsonicBookmarksForBookmarks(userBookmarkResult.Rows, trackListResult.Rows)
}
}
};
}
/// <summary>
/// Returns a cover art image.
/// </summary>
public async Task<subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>> GetCoverArt(subsonic.Request request, int? size)
{
var sw = Stopwatch.StartNew();
var result = new subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>
{
Data = new Roadie.Library.Models.Image()
};
if (request.ArtistId != null)
{
var artistImage = await this.ImageService.ArtistImage(request.ArtistId.Value, size, size);
if (!artistImage.IsSuccess)
{
return artistImage.Adapt<subsonic.SubsonicFileOperationResult<Image>>();
}
result.Data.Bytes = artistImage.Data.Bytes;
}
else if (request.TrackId != null)
{
var trackimage = await this.ImageService.TrackImage(request.TrackId.Value, size, size);
if (!trackimage.IsSuccess)
{
return trackimage.Adapt<subsonic.SubsonicFileOperationResult<Image>>();
}
result.Data.Bytes = trackimage.Data.Bytes;
}
else if (request.CollectionId != null)
{
var collectionImage = await this.ImageService.CollectionImage(request.CollectionId.Value, size, size);
if (!collectionImage.IsSuccess)
{
return collectionImage.Adapt<subsonic.SubsonicFileOperationResult<Image>>();
}
result.Data.Bytes = collectionImage.Data.Bytes;
}
else if (request.ReleaseId != null)
{
var releaseimage = await this.ImageService.ReleaseImage(request.ReleaseId.Value, size, size);
if (!releaseimage.IsSuccess)
{
return releaseimage.Adapt<subsonic.SubsonicFileOperationResult<Image>>();
}
result.Data.Bytes = releaseimage.Data.Bytes;
}
else if (request.PlaylistId != null)
{
var playlistImage = await this.ImageService.PlaylistImage(request.PlaylistId.Value, size, size);
if (!playlistImage.IsSuccess)
{
return playlistImage.Adapt<subsonic.SubsonicFileOperationResult<Image>>();
}
result.Data.Bytes = playlistImage.Data.Bytes;
}
else if (!string.IsNullOrEmpty(request.u))
{
var user = this.GetUser(request.u);
if (user == null)
{
return new subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Username [{ request.u}]");
}
var userImage = await this.ImageService.UserImage(user.RoadieId, size, size);
if (!userImage.IsSuccess)
{
return userImage.Adapt<subsonic.SubsonicFileOperationResult<Image>>();
}
result.Data.Bytes = userImage.Data.Bytes;
}
result.IsSuccess = result.Data.Bytes != null;
sw.Stop();
return new subsonic.SubsonicFileOperationResult<Roadie.Library.Models.Image>(result.Messages)
{
Data = result.Data,
ETag = result.ETag,
LastModified = result.LastModified,
ContentType = "image/jpeg",
Errors = result?.Errors,
IsSuccess = result.IsSuccess,
OperationTime = sw.ElapsedMilliseconds
};
}
/// <summary>
/// Returns all genres
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetGenres(subsonic.Request request)
{
var genres = (from g in this.DbContext.Genres
let albumCount = (from rg in this.DbContext.ReleaseGenres
where rg.GenreId == g.Id
select rg.Id).Count()
let songCount = (from rg in this.DbContext.ReleaseGenres
join rm in this.DbContext.ReleaseMedias on rg.ReleaseId equals rm.ReleaseId
join t in this.DbContext.Tracks on rm.ReleaseId equals t.ReleaseMediaId
where rg.GenreId == g.Id
select t.Id).Count()
select new subsonic.Genre
{
songCount = songCount,
albumCount = albumCount,
value = g.Name
}).OrderBy(x => x.value).ToArray();
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.genres,
Item = new subsonic.Genres
{
genre = genres.ToArray()
}
}
};
}
/// <summary>
/// Returns an indexed structure of all artists.
/// </summary>
/// <param name="request">Query from application.</param>
/// <param name="musicFolderId">If specified, only return artists in the music folder with the given ID.</param>
/// <param name="ifModifiedSince">If specified, only return a result if the artist collection has changed since the given time (in milliseconds since 1 Jan 1970).</param>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetIndexes(subsonic.Request request, User roadieUser, long? ifModifiedSince = null)
{
var cacheKey = string.Format("urn:subsonic_indexes");
return await this.CacheManager.GetAsync<subsonic.SubsonicOperationResult<subsonic.Response>>(cacheKey, async () =>
{
// Dont send the user to get index list as user data (likes, dislikes, etc.) aren't used in this list and dont need performance hit
return await this.GetIndexesAction(request, null, ifModifiedSince);
}, CacheManagerBase.SystemCacheRegionUrn);
}
/// <summary>
/// Get details about the software license. Takes no extra parameters. Roadies gives everyone a premium 1 year license everytime they ask :)
/// </summary>
public subsonic.SubsonicOperationResult<subsonic.Response> GetLicense(subsonic.Request request)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.license,
Item = new subsonic.License
{
email = this.Configuration.SmtpFromAddress,
valid = true,
licenseExpires = DateTime.UtcNow.AddYears(1),
licenseExpiresSpecified = true
}
}
};
}
/// <summary>
/// Searches for and returns lyrics for a given song
/// </summary>
public subsonic.SubsonicOperationResult<subsonic.Response> GetLyrics(subsonic.Request request, string artistId, string title)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.lyrics,
Item = new subsonic.Lyrics
{
artist = artistId,
title = title,
Text = new string[0]
}
}
};
}
/// <summary>
/// Returns a listing of all files in a music directory. Typically used to get list of albums for an artist, or list of songs for an album.
/// </summary>
/// <param name="request">Query from application.</param>
/// <param name="id">A string which uniquely identifies the music folder. Obtained by calls to getIndexes or getMusicDirectory.</param>
/// <returns></returns>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetMusicDirectory(subsonic.Request request, User roadieUser)
{
var directory = new subsonic.Directory();
var user = this.GetUser(roadieUser?.UserId);
// Request to get albums for an Artist
if (request.ArtistId != null)
{
var artistId = SafeParser.ToGuid(request.id);
var artist = this.GetArtist(artistId.Value);
if (artist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid ArtistId [{ request.id}]");
}
directory.id = subsonic.Request.ArtistIdIdentifier + artist.RoadieId.ToString();
directory.name = artist.Name;
var artistRating = user?.ArtistRatings?.FirstOrDefault(x => x.ArtistId == artist.Id);
if (artistRating?.IsFavorite ?? false)
{
directory.starred = (artistRating.LastUpdated ?? artistRating.CreatedDate);
directory.starredSpecified = true;
}
var pagedRequest = request.PagedRequest;
pagedRequest.FilterToArtistId = artist.RoadieId;
pagedRequest.Sort = "Release.Text";
var artistReleases = await this.ReleaseService.List(roadieUser, pagedRequest);
directory.child = this.SubsonicChildrenForReleases(artistReleases.Rows, subsonic.Request.ArtistIdIdentifier + artist.RoadieId.ToString());
}
// Request to get albums for in a Collection
else if (request.CollectionId != null)
{
var collectionId = SafeParser.ToGuid(request.id);
var collection = this.GetCollection(collectionId.Value);
if (collection == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid CollectionId [{ request.id}]");
}
directory.id = subsonic.Request.CollectionIdentifier + collection.RoadieId.ToString();
directory.name = collection.Name;
var pagedRequest = request.PagedRequest;
pagedRequest.FilterToCollectionId = collection.RoadieId;
var collectionReleases = await this.ReleaseService.List(roadieUser, pagedRequest);
directory.child = this.SubsonicChildrenForReleases(collectionReleases.Rows, subsonic.Request.CollectionIdentifier + collection.RoadieId.ToString());
}
// Request to get Tracks for an Album
else if (request.ReleaseId.HasValue)
{
var releaseId = SafeParser.ToGuid(request.id);
var release = this.GetRelease(releaseId.Value);
if (release == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid ReleaseId [{ request.id}]");
}
directory.id = subsonic.Request.ReleaseIdIdentifier + release.RoadieId.ToString();
directory.name = release.Title;
var releaseRating = user?.ReleaseRatings?.FirstOrDefault(x => x.ReleaseId == release.Id);
directory.averageRating = release.Rating ?? 0;
directory.parent = subsonic.Request.ArtistIdIdentifier + release.Artist.RoadieId.ToString();
if (releaseRating?.IsFavorite ?? false)
{
directory.starred = releaseRating.LastUpdated ?? releaseRating.CreatedDate;
directory.starredSpecified = true;
}
var pagedRequest = request.PagedRequest;
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();
}
else
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Unknown GetMusicDirectory Type [{ JsonConvert.SerializeObject(request) }], id [{ request.id }]");
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.directory,
Item = directory
}
};
}
/// <summary>
/// Returns all configured top-level music folders. Takes no extra parameters.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetMusicFolders(subsonic.Request request)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.musicFolders,
Item = new subsonic.MusicFolders
{
musicFolder = this.MusicFolders().ToArray()
}
}
};
}
/// <summary>
/// Returns a listing of files in a saved playlist.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetPlaylist(subsonic.Request request, User roadieUser)
{
var playListId = SafeParser.ToGuid(request.id);
if (!playListId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid PlaylistId [{ request.id }]");
}
var pagedRequest = request.PagedRequest;
pagedRequest.Sort = "Id";
pagedRequest.FilterToPlaylistId = playListId.Value;
var playlistResult = await this.PlaylistService.List(pagedRequest, roadieUser);
var playlist = playlistResult.Rows.Any() ? playlistResult.Rows.First() : null;
if (playlist == null)
{
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(pagedRequest, roadieUser);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.playlist,
Item = this.SubsonicPlaylistForPlaylist(playlist, tracksForPlaylist.Rows)
}
};
}
/// <summary>
/// Returns all playlists a user is allowed to play.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetPlaylists(subsonic.Request request, User roadieUser, string filterToUserName)
{
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>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.playlists,
Item = new subsonic.Playlists
{
playlist = this.SubsonicPlaylistsForPlaylists(playlistResult.Rows)
}
}
};
}
/// <summary>
/// Returns all Podcast channels the server subscribes to, and (optionally) their episodes. This method can also be used to return details for only one channel - refer to the id parameter. A typical use case for this method would be to first retrieve all channels without episodes, and then retrieve all episodes for the single channel the user selects.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetPodcasts(subsonic.Request request)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.podcasts,
Item = new subsonic.Podcasts
{
channel = new subsonic.PodcastChannel[0]
}
}
};
}
/// <summary>
/// Returns random songs matching the given criteria.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetRandomSongs(subsonic.Request request, User roadieUser)
{
var songs = new List<subsonic.Child>();
var randomSongs = await this.TrackService.List(request.PagedRequest, roadieUser, true);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.randomSongs,
Item = new subsonic.Songs
{
song = this.SubsonicChildrenForTracks(randomSongs.Rows)
}
}
};
}
/// <summary>
/// Returns a random collection of songs from the given artist and similar artists, using data from last.fm. Typically used for artist radio features.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetSimliarSongs(subsonic.Request request, User roadieUser, subsonic.SimilarSongsVersion version, int? count = 50)
{
// TODO How to determine similiar songs? Perhaps by genre?
switch (version)
{
case subsonic.SimilarSongsVersion.One:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.similarSongs,
Item = new subsonic.SimilarSongs
{
song = new subsonic.Child[0]
}
}
};
case subsonic.SimilarSongsVersion.Two:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.similarSongs2,
Item = new subsonic.SimilarSongs2
{
song = new subsonic.Child[0]
}
}
};
default:
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown SimilarSongsVersion [{ version }]");
}
}
/// <summary>
/// Returns details for a song.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetSong(subsonic.Request request, User roadieUser)
{
if (!request.TrackId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track [{ request.id }]");
}
var pagedRequest = request.PagedRequest;
pagedRequest.FilterToTrackId = request.TrackId.Value;
pagedRequest.Sort = "Id";
var trackResult = await this.TrackService.List(pagedRequest, roadieUser);
var track = trackResult.Rows.Any() ? trackResult.Rows.First() : null;
if (track == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track [{ request.id }]");
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.song,
Item = this.SubsonicChildForTrack(track)
}
};
}
/// <summary>
/// Returns songs in a given genre.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetSongsByGenre(subsonic.Request request, User roadieUser)
{
var pagedRequest = request.PagedRequest;
pagedRequest.FilterByGenre = request.Genre;
pagedRequest.Sort = "Id";
var trackResult = await this.TrackService.List(pagedRequest, roadieUser);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.songsByGenre,
Item = new subsonic.Songs
{
song = this.SubsonicChildrenForTracks(trackResult.Rows)
}
}
};
}
/// <summary>
/// Returns starred songs, albums and artists.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetStarred(subsonic.Request request, User roadieUser, subsonic.StarredVersion version)
{
var pagedRequest = request.PagedRequest;
pagedRequest.FilterFavoriteOnly = true;
pagedRequest.Sort = "Id";
var artistList = await this.ArtistService.List(roadieUser, pagedRequest);
var releaseList = await this.ReleaseService.List(roadieUser, pagedRequest);
var songList = await this.TrackService.List(pagedRequest, roadieUser);
switch (version)
{
case subsonic.StarredVersion.One:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.starred,
Item = new subsonic.Starred
{
album = this.SubsonicChildrenForReleases(releaseList.Rows, null),
artist = this.SubsonicArtistsForArtists(artistList.Rows),
song = this.SubsonicChildrenForTracks(songList.Rows)
}
}
};
case subsonic.StarredVersion.Two:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.starred2,
Item = new subsonic.Starred2
{
album = this.SubsonicAlbumID3ForReleases(releaseList.Rows),
artist = this.SubsonicArtistID3sForArtists(artistList.Rows),
song = this.SubsonicChildrenForTracks(songList.Rows)
}
}
};
default:
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown StarredVersion [{ version }]");
}
}
/// <summary>
/// Returns top songs for the given artist, using data from last.fm.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetTopSongs(subsonic.Request request, User roadieUser, int? count = 50)
{
data.Artist artist = null;
if (!string.IsNullOrEmpty(request.ArtistName))
{
artist = base.GetArtist(request.ArtistName);
}
else if (request.ArtistId.HasValue)
{
artist = this.GetArtist(request.ArtistId.Value);
}
if (artist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Unknown Artist [{ request.ArtistName }]");
}
var pagedRequest = request.PagedRequest;
pagedRequest.FilterToArtistId = artist.RoadieId;
pagedRequest.FilterTopPlayedOnly = true;
pagedRequest.Sort = "PlayedCount";
pagedRequest.Order = "DESC";
var trackResult = await this.TrackService.List(pagedRequest, roadieUser);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.topSongs,
Item = new subsonic.TopSongs
{
song = this.SubsonicChildrenForTracks(trackResult.Rows)
}
}
};
}
/// <summary>
/// Get details about a given user, including which authorization roles and folder access it has. Can be used to enable/disable certain features in the client, such as jukebox control.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetUser(subsonic.Request request, string username)
{
var user = this.GetUser(username);
if (user == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Username [{ username }]");
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.user,
Item = await this.SubsonicUserForUser(user)
}
};
}
/// <summary>
/// Returns all video files.
/// </summary>
public subsonic.SubsonicOperationResult<subsonic.Response> GetVideos(subsonic.Request request)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.videos,
Item = new subsonic.Videos
{
video = new subsonic.Child[0]
}
}
};
}
/// <summary>
/// Used to test connectivity with the server. Takes no extra parameters.
/// </summary>
public subsonic.SubsonicOperationResult<subsonic.Response> Ping(subsonic.Request request)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok
}
};
}
/// <summary>
/// Returns albums, artists and songs matching the given search criteria. Supports paging through the result.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> Search(subsonic.Request request, User roadieUser, subsonic.SearchVersion version)
{
var query = this.HttpEncoder.UrlDecode(request.Query).Replace("*", "").Replace("%", "").Replace(";", "");
// Search artists with query returning ArtistCount skipping ArtistOffset
var artistPagedRequest = request.PagedRequest;
artistPagedRequest.Sort = "Artist.Text";
artistPagedRequest.Limit = request.ArtistCount ?? artistPagedRequest.Limit;
artistPagedRequest.SkipValue = request.ArtistOffset ?? artistPagedRequest.SkipValue;
artistPagedRequest.Filter = query;
var artistResult = await this.ArtistService.List(roadieUser, artistPagedRequest);
// Search release with query returning RelaseCount skipping ReleaseOffset
var releasePagedRequest = request.PagedRequest;
releasePagedRequest.Sort = "Release.Text";
releasePagedRequest.Limit = request.AlbumCount ?? releasePagedRequest.Limit;
releasePagedRequest.SkipValue = request.AlbumOffset ?? releasePagedRequest.SkipValue;
releasePagedRequest.Filter = query;
var releaseResult = await this.ReleaseService.List(roadieUser, releasePagedRequest);
// Search tracks with query returning SongCount skipping SongOffset
var trackPagedRequest = request.PagedRequest;
trackPagedRequest.Sort = "Track.Text";
trackPagedRequest.Limit = request.SongCount ?? trackPagedRequest.Limit;
trackPagedRequest.SkipValue = request.SongOffset ?? trackPagedRequest.SkipValue;
trackPagedRequest.Filter = query;
var songResult = await this.TrackService.List(trackPagedRequest, roadieUser);
var songs = this.SubsonicChildrenForTracks(songResult.Rows);
switch (version)
{
case subsonic.SearchVersion.One:
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleClientRestProtocolVersion, "Deprecated since 1.4.0, use search2 instead.");
case subsonic.SearchVersion.Two:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.searchResult2,
Item = new subsonic.SearchResult2
{
artist = this.SubsonicArtistsForArtists(artistResult.Rows),
album = this.SubsonicChildrenForReleases(releaseResult.Rows, null),
song = songs.ToArray()
}
}
};
case subsonic.SearchVersion.Three:
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.searchResult3,
Item = new subsonic.SearchResult3
{
artist = this.SubsonicArtistID3sForArtists(artistResult.Rows),
album = this.SubsonicAlbumID3ForReleases(releaseResult.Rows),
song = songs.ToArray()
}
}
};
default:
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.IncompatibleServerRestProtocolVersion, $"Unknown SearchVersion [{ version }]");
}
}
/// <summary>
/// Sets the rating for a music file. If rating is zero then remove rating.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> SetRating(subsonic.Request request, User roadieUser, short rating)
{
var user = this.GetUser(roadieUser.UserId);
if (user == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.UserIsNotAuthorizedForGivenOperation, $"Invalid User [{ roadieUser }]");
}
// Id can be a song, album or artist
if (request.TrackId.HasValue)
{
var starResult = await this.SetTrackRating(request.TrackId.Value, user, rating);
if (starResult.IsSuccess)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response()
};
}
}
else if (request.ReleaseId.HasValue)
{
var starResult = await this.SetReleaseRating(request.ReleaseId.Value, user, rating);
if (starResult.IsSuccess)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response()
};
}
}
else if (request.ArtistId.HasValue)
{
var starResult = await this.SetArtistRating(request.ArtistId.Value, user, rating);
if (starResult.IsSuccess)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response()
};
}
}
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Unknown Star Id [{ JsonConvert.SerializeObject(request) }]");
}
/// <summary>
/// Attaches a star to a song, album or artist.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> ToggleStar(subsonic.Request request, User roadieUser, bool star, string[] albumIds = null, string[] artistIds = null)
{
var user = this.GetUser(roadieUser.UserId);
if (user == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.UserIsNotAuthorizedForGivenOperation, $"Invalid User [{ roadieUser }]");
}
// Id can be a song, album or artist
if (request.TrackId.HasValue)
{
var starResult = await this.ToggleTrackStar(request.TrackId.Value, user, star);
if (starResult.IsSuccess)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response()
};
}
}
else if (request.ReleaseId.HasValue)
{
var starResult = await this.ToggleReleaseStar(request.ReleaseId.Value, user, star);
if (starResult.IsSuccess)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response()
};
}
}
else if (request.ArtistId.HasValue)
{
var starResult = await this.ToggleArtistStar(request.ArtistId.Value, user, star);
if (starResult.IsSuccess)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response()
};
}
}
else if (albumIds != null && albumIds.Any())
{
foreach (var rId in albumIds)
{
var releaseId = SafeParser.ToGuid(rId);
if (releaseId.HasValue)
{
var starResult = await this.ToggleReleaseStar(releaseId.Value, user, star);
if (!starResult.IsSuccess)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(starResult.ErrorCode.Value, starResult.Messages.FirstOrDefault());
}
}
}
}
else if (artistIds != null && artistIds.Any())
{
foreach (var aId in artistIds)
{
var artistId = SafeParser.ToGuid(aId);
if (artistId.HasValue)
{
var starResult = await this.ToggleReleaseStar(artistId.Value, user, star);
if (!starResult.IsNotFoundResult)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(starResult.ErrorCode.Value, starResult.Messages.FirstOrDefault());
}
}
}
}
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Unknown Star Id [{ JsonConvert.SerializeObject(request) }]");
}
/// <summary>
/// Updates a playlist. Only the owner of a playlist is allowed to update it.
/// </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="comment">The playlist comment.</param>
/// <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)
{
request.id = playListId ?? request.id;
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 update playlist.");
}
playlist.Name = name ?? playlist.Name;
playlist.IsPublic = isPublic ?? playlist.IsPublic;
playlist.LastUpdated = DateTime.UtcNow;
if (songIdsToAdd != null && songIdsToAdd.Any())
{
// Add new if not already on Playlist
var songIdsToAddRoadieIds = songIdsToAdd.Select(x => SafeParser.ToGuid(x)).ToArray();
var submittedTracks = (from t in this.DbContext.Tracks
where songIdsToAddRoadieIds.Contains(t.RoadieId)
select t);
var listNumber = playlist.Tracks?.Max(x => x.ListNumber) ?? 0;
foreach (var submittedTrack in submittedTracks)
{
if (playlist.Tracks == null || 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
});
}
}
}
if (songIndexesToRemove != null && songIndexesToRemove.Any())
{
// Remove tracks from playlist
// 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();
var user = this.GetUser(roadieUser.UserId);
this.CacheManager.ClearRegion(user.CacheRegion);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok
}
};
}
/// <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", () =>
{
return this.DbContext.Users.Where(x => x.IsActive ?? false).Select(x => x.UserName).ToArray();
}, CacheManagerBase.SystemCacheRegionUrn);
}
private subsonic.MusicFolder CollectionMusicFolder()
{
return this.MusicFolders().First(x => x.id == 1);
}
private async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetIndexesAction(subsonic.Request request, User roadieUser, long? ifModifiedSince = null)
{
var modifiedSinceFilter = ifModifiedSince.HasValue ? (DateTime?)ifModifiedSince.Value.FromUnixTime() : null;
subsonic.MusicFolder musicFolderFilter = !request.MusicFolderId.HasValue ? new subsonic.MusicFolder() : this.MusicFolders().FirstOrDefault(x => x.id == request.MusicFolderId.Value);
var indexes = new List<subsonic.Index>();
if (musicFolderFilter.id == this.CollectionMusicFolder().id)
{
// Collections for Music Folders by Alpha First
foreach (var collectionFirstLetter in (from c in this.DbContext.Collections
let first = c.Name.Substring(0, 1)
orderby first
select first).Distinct().ToArray())
{
indexes.Add(new subsonic.Index
{
name = collectionFirstLetter,
artist = (from c in this.DbContext.Collections
where c.Name.Substring(0, 1) == collectionFirstLetter
where modifiedSinceFilter == null || c.LastUpdated >= modifiedSinceFilter
orderby c.SortName, c.Name
select new subsonic.Artist
{
id = subsonic.Request.CollectionIdentifier + c.RoadieId.ToString(),
name = c.Name,
artistImageUrl = this.MakeCollectionThumbnailImage(c.RoadieId).Url,
averageRating = 0,
userRating = 0
}).ToArray()
});
}
}
else
{
// Indexes for Artists alphabetically
var pagedRequest = request.PagedRequest;
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.Index
{
name = artistGroup.Key,
artist = this.SubsonicArtistsForArtists(artistGroup)
});
};
}
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
Data = new subsonic.Response
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.indexes,
Item = new subsonic.Indexes
{
lastModified = DateTime.UtcNow.ToUnixTime(),
index = indexes.ToArray()
}
}
};
}
private List<subsonic.MusicFolder> MusicFolders()
{
return new List<subsonic.MusicFolder>
{
new subsonic.MusicFolder { id = 1, name = "Collections"},
new subsonic.MusicFolder { id = 2, name = "Music"}
};
}
private subsonic.MusicFolder MusicMusicFolder()
{
return this.MusicFolders().First(x => x.id == 2);
}
private async Task<subsonic.SubsonicOperationResult<bool>> SetArtistRating(Guid artistId, ApplicationUser user, short rating)
{
var artist = this.GetArtist(artistId);
if (artist == null)
{
return new subsonic.SubsonicOperationResult<bool>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Artist Id [{ artistId }]");
}
var userArtist = user.ArtistRatings.FirstOrDefault(x => x.ArtistId == artist.Id);
if (userArtist == null)
{
userArtist = new data.UserArtist
{
Rating = rating,
UserId = user.Id,
ArtistId = artist.Id
};
this.DbContext.UserArtists.Add(userArtist);
}
else
{
userArtist.Rating = rating;
userArtist.LastUpdated = DateTime.UtcNow;
}
await this.DbContext.SaveChangesAsync();
this.CacheManager.ClearRegion(user.CacheRegion);
this.CacheManager.ClearRegion(artist.CacheRegion);
return new subsonic.SubsonicOperationResult<bool>
{
IsSuccess = true,
Data = true
};
}
private async Task<subsonic.SubsonicOperationResult<bool>> SetReleaseRating(Guid releaseId, ApplicationUser user, short rating)
{
var release = this.GetRelease(releaseId);
if (release == null)
{
return new subsonic.SubsonicOperationResult<bool>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release Id [{ releaseId }]");
}
var userRelease = user.ReleaseRatings.FirstOrDefault(x => x.ReleaseId == release.Id);
if (userRelease == null)
{
userRelease = new data.UserRelease
{
Rating = rating,
UserId = user.Id,
ReleaseId = release.Id
};
this.DbContext.UserReleases.Add(userRelease);
}
else
{
userRelease.Rating = rating;
userRelease.LastUpdated = DateTime.UtcNow;
}
await this.DbContext.SaveChangesAsync();
this.CacheManager.ClearRegion(user.CacheRegion);
this.CacheManager.ClearRegion(release.CacheRegion);
this.CacheManager.ClearRegion(release.Artist.CacheRegion);
return new subsonic.SubsonicOperationResult<bool>
{
IsSuccess = true,
Data = true
};
}
private async Task<subsonic.SubsonicOperationResult<bool>> SetTrackRating(Guid trackId, ApplicationUser user, short rating)
{
var track = this.GetTrack(trackId);
if (track == null)
{
return new subsonic.SubsonicOperationResult<bool>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track Id [{ trackId }]");
}
var userTrack = user.TrackRatings.FirstOrDefault(x => x.TrackId == track.Id);
if (userTrack == null)
{
userTrack = new data.UserTrack
{
Rating = rating,
UserId = user.Id,
TrackId = track.Id
};
this.DbContext.UserTracks.Add(userTrack);
}
else
{
userTrack.Rating = rating;
userTrack.LastUpdated = DateTime.UtcNow;
}
await this.DbContext.SaveChangesAsync();
this.CacheManager.ClearRegion(user.CacheRegion);
this.CacheManager.ClearRegion(track.CacheRegion);
this.CacheManager.ClearRegion(track.ReleaseMedia.Release.CacheRegion);
this.CacheManager.ClearRegion(track.ReleaseMedia.Release.Artist.CacheRegion);
return new subsonic.SubsonicOperationResult<bool>
{
IsSuccess = true,
Data = true
};
}
private subsonic.AlbumID3 SubsonicAlbumID3ForRelease(ReleaseList r)
{
return new subsonic.AlbumID3
{
id = subsonic.Request.ReleaseIdIdentifier + r.Id.ToString(),
artistId = r.Artist.Value,
name = r.Release.Text,
songCount = r.TrackCount ?? 0,
duration = r.Duration.ToSecondsFromMilliseconds(),
artist = r.Artist.Text,
coverArt = subsonic.Request.ReleaseIdIdentifier + r.Id.ToString(),
created = r.CreatedDate.Value,
genre = r.Genre.Text,
playCount = r.TrackPlayedCount ?? 0,
playCountSpecified = true,
starred = r.UserRating?.RatedDate ?? DateTime.UtcNow,
starredSpecified = r.UserRating?.IsFavorite ?? false,
year = SafeParser.ToNumber<int>(r.ReleaseYear),
yearSpecified = true
};
}
private subsonic.AlbumID3[] SubsonicAlbumID3ForReleases(IEnumerable<ReleaseList> r)
{
if (r == null || !r.Any())
{
return new subsonic.AlbumID3[0];
}
return r.Select(x => this.SubsonicAlbumID3ForRelease(x)).ToArray();
}
private subsonic.Artist SubsonicArtistForArtist(ArtistList artist)
{
return new subsonic.Artist
{
id = subsonic.Request.ArtistIdIdentifier + artist.Artist.Value.ToString(),
name = artist.Artist.Text,
artistImageUrl = this.MakeArtistThumbnailImage(artist.Id).Url,
averageRating = artist.Rating ?? 0,
averageRatingSpecified = true,
starred = artist.UserRating?.RatedDate ?? DateTime.UtcNow,
starredSpecified = artist.UserRating?.IsFavorite ?? false,
userRating = artist.UserRating != null ? artist.UserRating.Rating ?? 0 : 0,
userRatingSpecified = artist.UserRating != null && artist.UserRating.Rating != null
};
}
private subsonic.ArtistID3 SubsonicArtistID3ForArtist(ArtistList artist)
{
var artistImageUrl = this.MakeArtistThumbnailImage(artist.Id).Url;
return new subsonic.ArtistID3
{
id = subsonic.Request.ArtistIdIdentifier + artist.Artist.Value.ToString(),
name = artist.Artist.Text,
albumCount = artist.ArtistReleaseCount ?? 0,
coverArt = artistImageUrl,
artistImageUrl = artistImageUrl,
starred = artist.UserRating?.RatedDate ?? DateTime.UtcNow,
starredSpecified = artist.UserRating?.IsFavorite ?? false
};
}
private subsonic.ArtistID3[] SubsonicArtistID3sForArtists(IEnumerable<ArtistList> artists)
{
if (artists == null || !artists.Any())
{
return new subsonic.ArtistID3[0];
}
return artists.Select(x => this.SubsonicArtistID3ForArtist(x)).ToArray();
}
private subsonic.ArtistInfo2 SubsonicArtistInfo2InfoForArtist(data.Artist artist)
{
return new subsonic.ArtistInfo2
{
biography = artist.BioContext,
largeImageUrl = this.MakeImage(artist.RoadieId, "artist", this.Configuration.LargeImageSize).Url,
mediumImageUrl = this.MakeImage(artist.RoadieId, "artist", this.Configuration.MediumImageSize).Url,
musicBrainzId = artist.MusicBrainzId,
similarArtist = new subsonic.ArtistID3[0],
smallImageUrl = this.MakeImage(artist.RoadieId, "artist", this.Configuration.SmallImageSize).Url
};
}
private subsonic.ArtistInfo SubsonicArtistInfoForArtist(data.Artist artist)
{
return new subsonic.ArtistInfo
{
biography = artist.BioContext,
largeImageUrl = this.MakeImage(artist.RoadieId, "artist", this.Configuration.LargeImageSize).Url,
mediumImageUrl = this.MakeImage(artist.RoadieId, "artist", this.Configuration.MediumImageSize).Url,
musicBrainzId = artist.MusicBrainzId,
similarArtist = new subsonic.Artist[0],
smallImageUrl = this.MakeImage(artist.RoadieId, "artist", this.Configuration.SmallImageSize).Url
};
}
private subsonic.Artist[] SubsonicArtistsForArtists(IEnumerable<ArtistList> artists)
{
if (artists == null || !artists.Any())
{
return new subsonic.Artist[0];
}
return artists.Select(x => this.SubsonicArtistForArtist(x)).ToArray();
}
private subsonic.ArtistWithAlbumsID3 SubsonicArtistWithAlbumsID3ForArtist(ArtistList artist, subsonic.AlbumID3[] releases)
{
var artistImageUrl = this.MakeArtistThumbnailImage(artist.Id).Url;
return new subsonic.ArtistWithAlbumsID3
{
id = subsonic.Request.ArtistIdIdentifier + artist.Artist.Value.ToString(),
album = releases,
albumCount = releases.Count(),
artistImageUrl = artistImageUrl,
coverArt = artistImageUrl,
name = artist.Artist.Text,
starred = artist.UserRating?.RatedDate ?? DateTime.UtcNow,
starredSpecified = artist.UserRating?.IsFavorite ?? false
};
}
private subsonic.Bookmark SubsonicBookmarkForBookmark(BookmarkList b, subsonic.Child entry)
{
return new subsonic.Bookmark
{
changed = b.LastUpdated ?? b.CreatedDate.Value,
comment = b.Comment,
created = b.CreatedDate.Value,
position = b.Position ?? 0,
username = b.User.Text,
entry = entry
};
}
private subsonic.Bookmark[] SubsonicBookmarksForBookmarks(IEnumerable<BookmarkList> bb, IEnumerable<TrackList> childTracks)
{
if (bb == null || !bb.Any())
{
return new subsonic.Bookmark[0];
}
var result = new List<subsonic.Bookmark>();
foreach (var bookmark in bb)
{
subsonic.Child child = null;
switch (bookmark.Type.Value)
{
case Library.Enums.BookmarkType.Track:
child = this.SubsonicChildForTrack(childTracks.FirstOrDefault(x => x.Id == SafeParser.ToGuid(bookmark.Bookmark.Value)));
break;
default:
throw new NotImplementedException("Wrong Bookmark type to convert to Subsonic media Bookmark");
}
result.Add(this.SubsonicBookmarkForBookmark(bookmark, child));
}
return result.ToArray();
}
private subsonic.Child SubsonicChildForRelease(ReleaseList r, string parent, string path)
{
return new subsonic.Child
{
id = subsonic.Request.ReleaseIdIdentifier + r.Id.ToString(),
album = r.Release.Text,
albumId = subsonic.Request.ReleaseIdIdentifier + r.Id.ToString(),
artist = r.Artist.Text,
averageRating = r.Rating ?? 0,
averageRatingSpecified = true,
coverArt = subsonic.Request.ReleaseIdIdentifier + r.Id.ToString(),
created = r.CreatedDate.Value,
createdSpecified = true,
genre = r.Genre.Text,
isDir = true,
parent = parent ?? subsonic.Request.ArtistIdIdentifier + r.Artist.Value,
path = path,
playCount = r.TrackPlayedCount ?? 0,
playCountSpecified = true,
starred = r.UserRating?.RatedDate ?? DateTime.UtcNow,
starredSpecified = r.UserRating?.IsFavorite ?? false,
title = r.Release.Text,
userRating = r.UserRating != null ? r.UserRating.Rating ?? 0 : 0,
userRatingSpecified = r.UserRating != null && r.UserRating.Rating != null,
year = SafeParser.ToNumber<int>(r.ReleaseYear),
yearSpecified = true
};
}
private subsonic.Child SubsonicChildForTrack(TrackList t)
{
return new subsonic.Child
{
id = subsonic.Request.TrackIdIdentifier + t.Id.ToString(),
album = t.Release.Text,
albumId = subsonic.Request.ReleaseIdIdentifier + t.Release.Value,
artist = t.Artist.Text,
artistId = subsonic.Request.ArtistIdIdentifier + t.Artist.Value,
averageRating = t.Rating ?? 0,
averageRatingSpecified = true,
bitRate = 320,
bitRateSpecified = true,
contentType = "audio/mpeg",
coverArt = subsonic.Request.TrackIdIdentifier + t.Id.ToString(),
created = t.CreatedDate.Value,
createdSpecified = true,
discNumber = t.MediaNumber ?? 0,
discNumberSpecified = true,
duration = t.Duration.ToSecondsFromMilliseconds(),
durationSpecified = true,
isDir = false,
parent = subsonic.Request.ReleaseIdIdentifier + t.Release.Value,
path = $"{ t.Artist.Text }/{ t.Release.Text }/{ t.TrackNumber } - { t.Title }.mp3",
playCountSpecified = true,
size = t.FileSize ?? 0,
sizeSpecified = true,
starred = t.UserRating?.RatedDate ?? DateTime.UtcNow,
starredSpecified = t.UserRating?.IsFavorite ?? false,
suffix = "mp3",
title = t.Title,
track = t.TrackNumber ?? 0,
trackSpecified = t.TrackNumber.HasValue,
type = subsonic.MediaType.music,
typeSpecified = true,
userRating = t.UserRating != null ? t.UserRating.Rating ?? 0 : 0,
userRatingSpecified = t.UserRating != null,
year = t.Year ?? 0,
yearSpecified = t.Year.HasValue,
transcodedContentType = "audio/mpeg",
transcodedSuffix = "mp3",
isVideo = false,
isVideoSpecified = true,
playCount = t.PlayedCount ?? 0
};
}
private subsonic.Child[] SubsonicChildrenForReleases(IEnumerable<ReleaseList> r, string parent)
{
if (r == null || !r.Any())
{
return new subsonic.Child[0];
}
return r.Select(x => this.SubsonicChildForRelease(x, parent, $"{ x.Artist.Text}/{ x.Release.Text}/")).ToArray();
}
private subsonic.Child[] SubsonicChildrenForTracks(IEnumerable<TrackList> tracks)
{
if (tracks == null || !tracks.Any())
{
return new subsonic.Child[0];
}
return tracks.Select(x => this.SubsonicChildForTrack(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 = playlist.IsPublic ? this.AllowedUsers() : null,
changed = playlist.LastUpdated ?? playlist.CreatedDate ?? DateTime.UtcNow,
created = playlist.CreatedDate ?? DateTime.UtcNow,
duration = playlist.Duration ?? 0,
id = subsonic.Request.PlaylistdIdentifier + playlist.Id.ToString(),
name = playlist.Playlist.Text,
owner = playlist.User.Text,
@public = playlist.IsPublic,
publicSpecified = true,
songCount = playlist.PlaylistCount ?? 0,
entry = this.SubsonicChildrenForTracks(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 async Task<subsonic.User> SubsonicUserForUser(Library.Identity.ApplicationUser user)
{
var isAdmin = await this.UserManger.IsInRoleAsync(user, "Admin");
var isEditor = await this.UserManger.IsInRoleAsync(user, "Editor");
return new subsonic.User
{
adminRole = isAdmin,
avatarLastChanged = user.LastUpdated ?? user.CreatedDate ?? DateTime.UtcNow,
avatarLastChangedSpecified = user.LastUpdated.HasValue,
commentRole = true,
coverArtRole = isEditor || isAdmin,
downloadRole = isEditor || isAdmin, // Disable downloads
email = user.Email,
jukeboxRole = true,
maxBitRate = 320,
maxBitRateSpecified = true,
playlistRole = isEditor || isAdmin,
podcastRole = false, // Disable podcast nonsense
scrobblingEnabled = false, // Disable scrobbling
settingsRole = isAdmin,
shareRole = false, // TODO enabled when sharing is implemented
streamRole = true,
uploadRole = true,
username = user.UserName,
videoConversionRole = false, // Disable video nonsense
folder = this.MusicFolders().Select(x => x.id).ToArray()
};
}
private async Task<subsonic.SubsonicOperationResult<bool>> ToggleArtistStar(Guid artistId, ApplicationUser user, bool starred)
{
var artist = this.GetArtist(artistId);
if (artist == null)
{
return new subsonic.SubsonicOperationResult<bool>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Artist Id [{ artistId }]");
}
var userArtist = user.ArtistRatings.FirstOrDefault(x => x.ArtistId == artist.Id);
if (userArtist == null)
{
userArtist = new data.UserArtist
{
IsFavorite = true,
UserId = user.Id,
ArtistId = artist.Id
};
this.DbContext.UserArtists.Add(userArtist);
}
else
{
userArtist.IsFavorite = starred;
userArtist.LastUpdated = DateTime.UtcNow;
}
await this.DbContext.SaveChangesAsync();
this.CacheManager.ClearRegion(user.CacheRegion);
this.CacheManager.ClearRegion(artist.CacheRegion);
return new subsonic.SubsonicOperationResult<bool>
{
IsSuccess = true,
Data = true
};
}
private async Task<subsonic.SubsonicOperationResult<bool>> ToggleReleaseStar(Guid releaseId, ApplicationUser user, bool starred)
{
var release = this.GetRelease(releaseId);
if (release == null)
{
return new subsonic.SubsonicOperationResult<bool>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Release Id [{ releaseId }]");
}
var userRelease = user.ReleaseRatings.FirstOrDefault(x => x.ReleaseId == release.Id);
if (userRelease == null)
{
userRelease = new data.UserRelease
{
IsFavorite = true,
UserId = user.Id,
ReleaseId = release.Id
};
this.DbContext.UserReleases.Add(userRelease);
}
else
{
userRelease.IsFavorite = starred;
userRelease.LastUpdated = DateTime.UtcNow;
}
await this.DbContext.SaveChangesAsync();
this.CacheManager.ClearRegion(user.CacheRegion);
this.CacheManager.ClearRegion(release.CacheRegion);
this.CacheManager.ClearRegion(release.Artist.CacheRegion);
return new subsonic.SubsonicOperationResult<bool>
{
IsSuccess = true,
Data = true
};
}
private async Task<subsonic.SubsonicOperationResult<bool>> ToggleTrackStar(Guid trackId, ApplicationUser user, bool starred)
{
var track = this.GetTrack(trackId);
if (track == null)
{
return new subsonic.SubsonicOperationResult<bool>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track Id [{ trackId }]");
}
var userTrack = user.TrackRatings.FirstOrDefault(x => x.TrackId == track.Id);
if (userTrack == null)
{
userTrack = new data.UserTrack
{
IsFavorite = true,
UserId = user.Id,
TrackId = track.Id
};
this.DbContext.UserTracks.Add(userTrack);
}
else
{
userTrack.IsFavorite = starred;
userTrack.LastUpdated = DateTime.UtcNow;
}
await this.DbContext.SaveChangesAsync();
this.CacheManager.ClearRegion(user.CacheRegion);
this.CacheManager.ClearRegion(track.CacheRegion);
this.CacheManager.ClearRegion(track.ReleaseMedia.Release.CacheRegion);
this.CacheManager.ClearRegion(track.ReleaseMedia.Release.Artist.CacheRegion);
return new subsonic.SubsonicOperationResult<bool>
{
IsSuccess = true,
Data = true
};
}
#endregion Privates
}
}