Subsonic API Work. Removed some non dotnet core packages.

This commit is contained in:
Steven Hildreth 2018-11-25 10:57:17 -06:00
parent d8b096b543
commit 4a62537a42
16 changed files with 700 additions and 360 deletions

View file

@ -87,6 +87,35 @@ namespace Roadie.Api.Controllers
return this.BuildResponse(request, result);
}
[HttpGet("deletePlaylist.view")]
[HttpPost("deletePlaylist.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> DeletePlaylist(SubsonicRequest request)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.DeletePlaylist(request, this.SubsonicUser);
return this.BuildResponse(request, result);
}
[HttpGet("updatePlaylist.view")]
[HttpPost("updatePlaylist.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> UpdatePlaylist(SubsonicRequest request, string playlistId, string name, string comment, bool? @public, string[] songIdToAdd, int[] songIndexToRemove)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.UpdatePlaylist(request, this.SubsonicUser, playlistId, name, comment, @public, songIdToAdd, songIndexToRemove);
return this.BuildResponse(request, result);
}
[HttpGet("getBookmarks.view")]
[HttpPost("getBookmarks.view")]
[ProducesResponseType(200)]
@ -364,11 +393,31 @@ namespace Roadie.Api.Controllers
return this.BuildResponse(request, result, "playlist");
}
[HttpGet("createPlaylist.view")]
[HttpPost("createPlaylist.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> CreatePlaylist(SubsonicRequest request, string playlistId, string name, string[] songId)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.CreatePlaylist(request, this.SubsonicUser, name, songId, playlistId);
return this.BuildResponse(request, result, "playlist");
}
[HttpGet("getPlaylists.view")]
[HttpPost("getPlaylists.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> GetPlaylists(SubsonicRequest request, string username)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetPlaylists(request, this.SubsonicUser, username);
return this.BuildResponse(request, result, "playlists");
}
@ -378,6 +427,11 @@ namespace Roadie.Api.Controllers
[ProducesResponseType(200)]
public async Task<IActionResult> GetPodcasts(SubsonicRequest request, bool includeEpisodes)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetPodcasts(request);
return this.BuildResponse(request, result, "podcasts");
}
@ -483,14 +537,14 @@ namespace Roadie.Api.Controllers
[HttpGet("getTopSongs.view")]
[HttpPost("getTopSongs.view")]
[ProducesResponseType(200)]
public async Task<IActionResult> GetTopSongs(SubsonicRequest request, string artist, int? count = 50)
public async Task<IActionResult> GetTopSongs(SubsonicRequest request, int? count = 50)
{
var authResult = await this.AuthenticateUser(request);
if (authResult != null)
{
return authResult;
}
var result = await this.SubsonicService.GetTopSongs(request, this.SubsonicUser, artist, count);
var result = await this.SubsonicService.GetTopSongs(request, this.SubsonicUser, count);
return this.BuildResponse(request, result, "topSongs");
}
@ -667,7 +721,7 @@ namespace Roadie.Api.Controllers
}
postBody = JsonConvert.SerializeObject(formDictionary);
}
this.Logger.LogTrace($"Subsonic Request: Method [{ method }], Accept Header [{ acceptHeader }], Path [{ queryPath }], Query String [{ queryString }], Posted Body [{ postBody }], Response Error Code [{ response?.ErrorCode }], Request [{ JsonConvert.SerializeObject(request) }] ResponseType [{ responseType }]");
this.Logger.LogTrace($"Subsonic Request: Method [{ method }], Accept Header [{ acceptHeader }], Path [{ queryPath }], Query String [{ queryString }], Posted Body [{ postBody }], Response Error Code [{ response?.ErrorCode }], Request [{ JsonConvert.SerializeObject(request, Formatting.Indented) }] ResponseType [{ responseType }]");
if (response?.ErrorCode.HasValue ?? false)
{
return this.SendError(request, response);

View file

@ -25,34 +25,36 @@ namespace Roadie.Api.ModelBinding
// Create a dictionary of all the properties to populate on the result model
var modelDictionary = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase)
{
{ "u", null },
{ "p", null },
{ "s", null },
{ "t", null },
{ "v", null },
{ "c", null },
{ "id", null },
{ "f", null },
{ "callback", null },
{ "musicFolderId", null },
{ "albumCount", null },
{ "albumOffset", null },
{ "artist", null },
{ "artistCount", null },
{ "artistOffset", null },
{ "c", null },
{ "callback", null },
{ "f", null },
{ "fromYear", null },
{ "genre", null },
{ "id", null },
{ "musicFolderId", null },
{ "offset", null },
{ "p", null },
{ "query", null },
{ "s", null },
{ "size", null },
{ "songCount", null },
{ "songOffset", null },
{ "t", null },
{ "toYear", null },
{ "type", null }
{ "type", null },
{ "u", null },
{ "v", null }
};
// Setup model dictionary from Query Parameters
modelDictionary["albumCount"] = queryDictionary.ContainsKey("albumCount") ? SafeParser.ToNumber<int?>(queryDictionary["albumCount"].First()) : null;
modelDictionary["albumOffset"] = queryDictionary.ContainsKey("albumOffset") ? SafeParser.ToNumber<int?>(queryDictionary["albumOffset"].First()) : null;
modelDictionary["artist"] = queryDictionary.ContainsKey("artist") ? queryDictionary["artist"].First() : null;
modelDictionary["artistCount"] = queryDictionary.ContainsKey("artistCount") ? SafeParser.ToNumber<int?>(queryDictionary["artistCount"].First()) : null;
modelDictionary["artistOffset"] = queryDictionary.ContainsKey("artistOffset") ? SafeParser.ToNumber<int?>(queryDictionary["artistOffset"].First()) : null;
modelDictionary["c"] = queryDictionary.ContainsKey("c") ? queryDictionary["c"].First() : null;
@ -102,6 +104,7 @@ namespace Roadie.Api.ModelBinding
AlbumCount = SafeParser.ToNumber<int?>(modelDictionary["albumCount"]) ?? 20,
AlbumOffset = SafeParser.ToNumber<int?>(modelDictionary["albumOffset"]),
ArtistCount = SafeParser.ToNumber<int?>(modelDictionary["artistCount"]) ?? 20,
ArtistName = SafeParser.ToString(modelDictionary["artist"]),
ArtistOffset = SafeParser.ToNumber<int?>(modelDictionary["artistOffset"]),
c = SafeParser.ToString(modelDictionary["c"]),
callback = SafeParser.ToString(modelDictionary["callback"]),

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<WebPublishMethod>FileSystem</WebPublishMethod>
<PublishProvider>FileSystem</PublishProvider>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>x64</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
<ExcludeApp_Data>False</ExcludeApp_Data>
<ProjectGuid>68c80416-0d72-409d-b727-3fea7ab7fd2c</ProjectGuid>
<publishUrl>bin\x64\Debug\netcoreapp2.1\publish\</publishUrl>
<DeleteExistingFiles>False</DeleteExistingFiles>
<TargetFramework>netcoreapp2.1</TargetFramework>
<SelfContained>false</SelfContained>
<_IsPortable>true</_IsPortable>
</PropertyGroup>
</Project>

View file

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

View file

@ -308,7 +308,7 @@ namespace Roadie.Api.Services
};
}
public async Task<Library.Models.Pagination.PagedResult<ArtistList>> List(User roadieUser, PagedRequest request, bool? doRandomize = false)
public async Task<Library.Models.Pagination.PagedResult<ArtistList>> List(User roadieUser, PagedRequest request, bool? doRandomize = false, bool? onlyIncludeWithReleases = true, bool? doArtistCounts = true)
{
var sw = new Stopwatch();
sw.Start();
@ -322,8 +322,9 @@ namespace Roadie.Api.Services
select a.Id
).ToArray();
}
var onlyWithReleases = onlyIncludeWithReleases ?? true;
var result = (from a in this.DbContext.Artists
where (!onlyWithReleases || a.ReleaseCount > 0)
where (request.FilterToArtistId == null || a.RoadieId == request.FilterToArtistId)
where (request.FilterMinimumRating == null || a.Rating >= request.FilterMinimumRating.Value)
where (request.FilterValue == "" || (a.Name.Contains(request.FilterValue) || a.SortName.Contains(request.FilterValue) || a.AlternateNames.Contains(request.FilterValue)))
@ -367,6 +368,27 @@ namespace Roadie.Api.Services
}
rows = result.OrderBy(sortBy).Skip(request.SkipValue).Take(request.LimitValue).ToArray();
}
if(rows.Any() && (doArtistCounts ?? true))
{
var rowArtistIds = rows.Select(x => x.DatabaseId);
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,
r.TrackCount,
r.PlayedCount
}).ToList();
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);
}
}
if (rows.Any() && roadieUser != null)
{
foreach (var userArtistRating in this.GetUser(roadieUser.UserId).ArtistRatings.Where(x => rows.Select(r => r.DatabaseId).Contains(x.ArtistId)))

View file

@ -12,6 +12,6 @@ namespace Roadie.Api.Services
{
Task<OperationResult<Artist>> ById(User roadieUser, Guid id, IEnumerable<string> includes);
Task<PagedResult<ArtistList>> List(User roadieUser, PagedRequest request, bool? doRandomize = false);
Task<PagedResult<ArtistList>> List(User roadieUser, PagedRequest request, bool? doRandomize = false, bool? onlyIncludeWithReleases = true, bool? doArtistCounts = true);
}
}

View file

@ -9,8 +9,10 @@ namespace Roadie.Api.Services
Task<SubsonicOperationResult<SubsonicAuthenticateResponse>> Authenticate(Request request);
Task<SubsonicOperationResult<Response>> CreateBookmark(Request request, Roadie.Library.Models.Users.User roadieUser, int position, string comment);
Task<SubsonicOperationResult<Response>> CreatePlaylist(Request request, Roadie.Library.Models.Users.User roadieUser, string name, string[] songIds, string playlistId = null);
Task<SubsonicOperationResult<Response>> DeleteBookmark(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<SubsonicOperationResult<Response>> DeletePlaylist(Request request, Roadie.Library.Models.Users.User roadieUser);
Task<SubsonicOperationResult<Response>> GetAlbum(Request request, Roadie.Library.Models.Users.User roadieUser);
@ -56,7 +58,7 @@ namespace Roadie.Api.Services
Task<SubsonicOperationResult<Response>> GetStarred(Request request, Roadie.Library.Models.Users.User roadieUser, StarredVersion version);
Task<SubsonicOperationResult<Response>> GetTopSongs(Request request, Roadie.Library.Models.Users.User roadieUser, string artistName, int? count = 50);
Task<SubsonicOperationResult<Response>> GetTopSongs(Request request, Roadie.Library.Models.Users.User roadieUser, int? count = 50);
Task<SubsonicOperationResult<Response>> GetUser(Request request, string username);
@ -68,5 +70,7 @@ namespace Roadie.Api.Services
Task<SubsonicOperationResult<Response>> ToggleStar(Request request, Roadie.Library.Models.Users.User roadieUser, bool star, string[] albumIds = null, string[] artistIds = null);
Task<SubsonicOperationResult<Response>> SetRating(Request request, Roadie.Library.Models.Users.User roadieUser, short rating);
Task<SubsonicOperationResult<Response>> UpdatePlaylist(Request request, Roadie.Library.Models.Users.User roadieUser, string playlistId, string name = null, string comment = null, bool? isPublic = null, string[] songIdsToAdd = null, int[] songIndexesToRemove = null);
}
}

View file

@ -1,5 +1,6 @@
using Mapster;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Roadie.Library.Caching;
@ -129,7 +130,7 @@ namespace Roadie.Api.Services
this.Logger.LogInformation($"Unknown User [{ request.u }]");
return new subsonic.SubsonicOperationResult<subsonic.SubsonicAuthenticateResponse>(subsonic.ErrorCodes.WrongUsernameOrPassword, $"Unknown Username");
}
this.Logger.LogInformation($"Successfully Authenticated User [{ user.ToString() }]");
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,
@ -146,6 +147,94 @@ namespace Roadie.Api.Services
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>
/// 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>
/// Returns details for an album, including a list of songs. This method organizes music according to ID3 tags.
/// </summary>
@ -414,7 +503,11 @@ namespace Roadie.Api.Services
pagedRequest.SkipValue = 0;
pagedRequest.Limit = int.MaxValue;
pagedRequest.Sort = "Artist.Text";
var artistList = await this.ArtistService.List(roadieUser, pagedRequest);
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
@ -440,6 +533,33 @@ namespace Roadie.Api.Services
};
}
/// <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(roadieUser, pagedRequest);
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>
@ -569,14 +689,11 @@ namespace Roadie.Api.Services
/// <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)
{
if (roadieUser != null)
{
return await this.GetIndexesAction(request, roadieUser, ifModifiedSince);
}
var cacheKey = string.Format("urn:subsonic_indexes");
return await this.CacheManager.GetAsync<subsonic.SubsonicOperationResult<subsonic.Response>>(cacheKey, async () =>
{
return await this.GetIndexesAction(request, roadieUser, ifModifiedSince);
// 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);
}
@ -1008,12 +1125,20 @@ namespace Roadie.Api.Services
/// <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, string artistName, int? count = 50)
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetTopSongs(subsonic.Request request, User roadieUser, int? count = 50)
{
var artist = base.GetArtist(artistName);
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 [{ artistName }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Unknown Artist [{ request.ArtistName }]");
}
var pagedRequest = request.PagedRequest;
pagedRequest.FilterToArtistId = artist.RoadieId;
@ -1308,73 +1433,117 @@ namespace Roadie.Api.Services
}
/// <summary>
/// Returns all bookmarks for this user. A bookmark is a position within a certain media file.
/// Creates (or updates) a playlist.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> GetBookmarks(subsonic.Request request, User roadieUser)
/// <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)
{
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(roadieUser, pagedRequest);
return new subsonic.SubsonicOperationResult<subsonic.Response>
data.Playlist playlist = null;
Guid?[] songRoadieIds = new Guid?[0];
IQueryable<data.Track> submittedTracks = new data.Track[0].AsQueryable();
if (songIds != null && songIds.Any())
{
IsSuccess = true,
Data = new subsonic.Response
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)
{
version = SubsonicService.SubsonicVersion,
status = subsonic.ResponseStatus.ok,
ItemElementName = subsonic.ItemChoiceType.bookmarks,
Item = new subsonic.Bookmarks
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))
{
bookmark = this.SubsonicBookmarksForBookmarks(userBookmarkResult.Rows, trackListResult.Rows)
listNumber++;
this.DbContext.PlaylistTracks.Add(new data.PlaylistTrack
{
PlayListId = playlist.Id,
ListNumber = listNumber,
TrackId = submittedTrack.Id
});
}
}
};
}
/// <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;
playlist.Name = name ?? playlist.Name;
playlist.LastUpdated = DateTime.UtcNow;
}
else
{
userBookmark.LastUpdated = DateTime.UtcNow;
userBookmark.Position = position;
userBookmark.Comment = comment;
var tracks = new List<data.PlaylistTrack>();
var listNumber = 0;
foreach (var submittedTrack in submittedTracks)
{
listNumber++;
tracks.Add(new data.PlaylistTrack
{
PlayListId = playlist.Id,
ListNumber = listNumber,
TrackId = submittedTrack.Id
});
}
playlist = new data.Playlist
{
IsPublic = false,
Name = name,
UserId = roadieUser.Id,
Tracks = tracks
};
didCreate = true;
this.DbContext.Playlists.Add(playlist);
}
await this.DbContext.SaveChangesAsync();
this.Logger.LogInformation($"Subsonic: User `{ roadieUser }` { (didCreate ? "created": "modified") } Playlist `{ playlist }` added [{ songRoadieIds.Count() }] Tracks.");
request.id = subsonic.Request.PlaylistdIdentifier + playlist.RoadieId.ToString();
return await this.GetPlaylist(request, roadieUser);
}
/// <summary>
/// Deletes a saved playlist.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> DeletePlaylist(subsonic.Request request, User roadieUser)
{
if (!request.PlaylistId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.id }]");
}
var playlist = this.GetPlaylist(request.PlaylistId.Value);
if (playlist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.TrackId.Value }]");
}
if(playlist.UserId != roadieUser.Id && !roadieUser.IsAdmin)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.UserIsNotAuthorizedForGivenOperation, "User is not allowed to delete playlist.");
}
this.DbContext.Playlists.Remove(playlist);
await this.DbContext.SaveChangesAsync();
var user = this.GetUser(roadieUser.UserId);
this.CacheManager.ClearRegion(user.CacheRegion);
this.Logger.LogInformation($"{ (createdBookmark ? "Created" : "Updated") } Bookmark `{ userBookmark}` for User `{ roadieUser }`");
this.Logger.LogInformation($"Subsonic: Deleted Playlist `{ playlist}` for User `{ roadieUser }`");
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
@ -1387,31 +1556,82 @@ namespace Roadie.Api.Services
}
/// <summary>
/// Deletes the bookmark for a given file.
/// Updates a playlist. Only the owner of a playlist is allowed to update it.
/// </summary>
public async Task<subsonic.SubsonicOperationResult<subsonic.Response>> DeleteBookmark(subsonic.Request request, User roadieUser)
/// <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)
{
if (!request.TrackId.HasValue)
request.id = playListId ?? request.id;
if (!request.PlaylistId.HasValue)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track Id [{ request.id }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist Id [{ request.id }]");
}
var track = this.GetTrack(request.TrackId.Value);
if (track == null)
var playlist = this.GetPlaylist(request.PlaylistId.Value);
if (playlist == null)
{
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Track Id [{ request.TrackId.Value }]");
return new subsonic.SubsonicOperationResult<subsonic.Response>(subsonic.ErrorCodes.TheRequestedDataWasNotFound, $"Invalid Playlist 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)
if (playlist.UserId != roadieUser.Id && !roadieUser.IsAdmin)
{
this.DbContext.Bookmarks.Remove(userBookmark);
await this.DbContext.SaveChangesAsync();
var user = this.GetUser(roadieUser.UserId);
this.CacheManager.ClearRegion(user.CacheRegion);
this.Logger.LogInformation($"Deleted Bookmark `{ userBookmark}` for User `{ roadieUser }`");
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
//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
// });
// }
//}
}
await this.DbContext.SaveChangesAsync();
var user = this.GetUser(roadieUser.UserId);
this.CacheManager.ClearRegion(user.CacheRegion);
return new subsonic.SubsonicOperationResult<subsonic.Response>
{
IsSuccess = true,
@ -1426,6 +1646,19 @@ namespace Roadie.Api.Services
#region Privates
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;
@ -1465,13 +1698,17 @@ namespace Roadie.Api.Services
pagedRequest.SkipValue = 0;
pagedRequest.Limit = int.MaxValue;
pagedRequest.Sort = "Artist.Text";
var artistList = await this.ArtistService.List(roadieUser, pagedRequest);
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)
artist = this.SubsonicArtistsForArtists(artistGroup)
});
};
}
@ -1485,13 +1722,27 @@ namespace Roadie.Api.Services
ItemElementName = subsonic.ItemChoiceType.indexes,
Item = new subsonic.Indexes
{
lastModified = DateTime.UtcNow.ToUnixTime(),
index = indexes.ToArray()
// TODO child
}
}
};
}
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);
@ -1600,177 +1851,6 @@ namespace Roadie.Api.Services
};
}
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
};
}
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 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 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.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.AlbumID3 SubsonicAlbumID3ForRelease(ReleaseList r)
{
return new subsonic.AlbumID3
@ -1893,6 +1973,43 @@ namespace Roadie.Api.Services
};
}
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
@ -2035,7 +2152,7 @@ namespace Roadie.Api.Services
podcastRole = false, // Disable podcast nonsense
scrobblingEnabled = false, // Disable scrobbling
settingsRole = isAdmin,
shareRole = false, // TODO enabled when sharing is implmeneted
shareRole = false, // TODO enabled when sharing is implemented
streamRole = true,
uploadRole = true,
username = user.UserName,
@ -2044,6 +2161,114 @@ namespace Roadie.Api.Services
};
}
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
}
}

View file

@ -7,7 +7,7 @@
}
},
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.RollingFileAlternate", "Serilog.Sinks.SQLite" ],
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.RollingFileAlternate" ],
"MinimumLevel": {
"Default": "Verbose",
"Override": {
@ -31,13 +31,6 @@
"fileSizeLimitBytes": 26214400,
"buffered": true
}
},
{
"Name": "SQLite",
"Args": {
"restrictedToMinimumLevel": "Error",
"sqliteDbPath": "logs\\errors.sqlite"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId", "WithExceptionDetails" ],

View file

@ -66,6 +66,8 @@ namespace Roadie.Library.Data
[MaxLength(100)]
public string SpotifyId { get; set; }
[Column("releaseCount")]
public int? ReleaseCount { get; set; } // TODO update this on artist folder scan
}
}

View file

@ -23,5 +23,10 @@ namespace Roadie.Library.Data
return Playlist.CacheUrn(this.RoadieId);
}
}
public override string ToString()
{
return $"Id [{this.Id}], Name [{this.Name}], RoadieId [{ this.RoadieId}]";
}
}
}

View file

@ -26,7 +26,7 @@ namespace Roadie.Library.Data
public ICollection<CollectionRelease> Collections { get; set; }
[Column("duration")]
public int? Duration { get; set; }
public int? Duration { get; set; } // TODO update this on release scan
[Column("discogsId")]
[MaxLength(50)]
@ -69,7 +69,7 @@ namespace Roadie.Library.Data
public string MusicBrainzId { get; set; }
[Column("playedCount")]
public int? PlayedCount { get; set; }
public int? PlayedCount { get; set; }
[Column("profile", TypeName = "text")]
[MaxLength(65535)]
@ -104,7 +104,7 @@ namespace Roadie.Library.Data
public string Title { get; set; }
[Column("trackCount")]
public short TrackCount { get; set; }
public short TrackCount { get; set; }
[Column("urls", TypeName = "text")]
[MaxLength(65535)]

View file

@ -211,6 +211,12 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic
/// </summary>
public int? ArtistOffset { get; set; }
/// <summary>
/// The artist name.
/// <see cref="getTopSongs"/>
/// </summary>
public string ArtistName { get; set; }
/// <summary>
/// The first year in the range. If fromYear > toYear a reverse chronological list is returned.
/// </summary>

View file

@ -145,10 +145,11 @@ namespace Roadie.Library.Processors
var extension = file.Extension.ToLower();
if (extension.Equals(".mp3") || extension.Equals(".flac"))
{
var tagFile = TagLib.File.Create(file.FullName);
tagFile.Tag.Performers = null;
tagFile.Tag.Performers = new[] { artist };
tagFile.Save();
// TODO
//var tagFile = TagLib.File.Create(file.FullName);
//tagFile.Tag.Performers = null;
//tagFile.Tag.Performers = new[] { artist };
//tagFile.Save();
}
}
}

View file

@ -8,13 +8,14 @@
<ItemGroup>
<PackageReference Include="FluentFTP" Version="19.2.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.8.10" />
<PackageReference Include="ID3" Version="0.5.0-beta.1" />
<PackageReference Include="ID3Tag.Core" Version="0.1.3" />
<PackageReference Include="Inflatable.Lastfm" Version="1.1.0.339" />
<PackageReference Include="Mapster" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Caching.Redis" Version="2.1.2" />
<PackageReference Include="MimeMapping" Version="1.0.1.12" />
<PackageReference Include="Orthogonal.NTagLite" Version="2.0.9" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
<PackageReference Include="RestSharp" Version="106.5.4" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0006" />
@ -22,7 +23,6 @@
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0005" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0007" />
<PackageReference Include="System.Runtime.Caching" Version="4.5.0" />
<PackageReference Include="taglib" Version="2.1.0" />
</ItemGroup>
<ItemGroup>

View file

@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging;
using Orthogonal.NTagLite;
using Roadie.Library.Caching;
using Roadie.Library.Configuration;
using Roadie.Library.Extensions;
@ -70,28 +69,30 @@ namespace Roadie.Library.MetaData.ID3Tags
{
try
{
var tagFile = TagLib.File.Create(filename);
tagFile.Tag.AlbumArtists = null;
tagFile.Tag.AlbumArtists = new[] { metaData.Artist };
tagFile.Tag.Performers = null;
if (metaData.TrackArtists.Any())
{
tagFile.Tag.Performers = metaData.TrackArtists.ToArray();
}
tagFile.Tag.Album = metaData.Release;
tagFile.Tag.Title = metaData.Title;
tagFile.Tag.Year = force ? (uint)(metaData.Year ?? 0) : tagFile.Tag.Year > 0 ? tagFile.Tag.Year : (uint)(metaData.Year ?? 0);
tagFile.Tag.Track = force ? (uint)(metaData.TrackNumber ?? 0) : tagFile.Tag.Track > 0 ? tagFile.Tag.Track : (uint)(metaData.TrackNumber ?? 0);
tagFile.Tag.TrackCount = force ? (uint)(metaData.TotalTrackNumbers ?? 0) : tagFile.Tag.TrackCount > 0 ? tagFile.Tag.TrackCount : (uint)(metaData.TotalTrackNumbers ?? 0);
tagFile.Tag.Disc = force ? (uint)(metaData.Disk ?? 0) : tagFile.Tag.Disc > 0 ? tagFile.Tag.Disc : (uint)(metaData.Disk ?? 0);
tagFile.Tag.Pictures = metaData.Images == null ? null : metaData.Images.Select(x => new TagLib.Picture
{
Data = new TagLib.ByteVector(x.Data),
Description = x.Description,
MimeType = x.MimeType,
Type = (TagLib.PictureType)x.Type
}).ToArray();
tagFile.Save();
// TODO
//var tagFile = TagLib.File.Create(filename);
//tagFile.Tag.AlbumArtists = null;
//tagFile.Tag.AlbumArtists = new[] { metaData.Artist };
//tagFile.Tag.Performers = null;
//if (metaData.TrackArtists.Any())
//{
// tagFile.Tag.Performers = metaData.TrackArtists.ToArray();
//}
//tagFile.Tag.Album = metaData.Release;
//tagFile.Tag.Title = metaData.Title;
//tagFile.Tag.Year = force ? (uint)(metaData.Year ?? 0) : tagFile.Tag.Year > 0 ? tagFile.Tag.Year : (uint)(metaData.Year ?? 0);
//tagFile.Tag.Track = force ? (uint)(metaData.TrackNumber ?? 0) : tagFile.Tag.Track > 0 ? tagFile.Tag.Track : (uint)(metaData.TrackNumber ?? 0);
//tagFile.Tag.TrackCount = force ? (uint)(metaData.TotalTrackNumbers ?? 0) : tagFile.Tag.TrackCount > 0 ? tagFile.Tag.TrackCount : (uint)(metaData.TotalTrackNumbers ?? 0);
//tagFile.Tag.Disc = force ? (uint)(metaData.Disk ?? 0) : tagFile.Tag.Disc > 0 ? tagFile.Tag.Disc : (uint)(metaData.Disk ?? 0);
//tagFile.Tag.Pictures = metaData.Images == null ? null : metaData.Images.Select(x => new TagLib.Picture
//{
// Data = new TagLib.ByteVector(x.Data),
// Description = x.Description,
// MimeType = x.MimeType,
// Type = (TagLib.PictureType)x.Type
//}).ToArray();
//tagFile.Save();
return true;
}
catch (Exception ex)
@ -109,31 +110,33 @@ namespace Roadie.Library.MetaData.ID3Tags
var isSuccess = false;
try
{
var file = LiteFile.LoadFromFile(fileName);
var tpos = file.Tag.FindFirstFrameById(FrameId.TPOS);
Picture[] pics = file.Tag.FindFramesById(FrameId.APIC).Select(f => f.GetPicture()).ToArray();
result.Release = file.Tag.Album;
result.Artist = file.Tag.Artist;
result.ArtistRaw = file.Tag.Artist;
result.Genres = (file.Tag.Genre ?? string.Empty).Split(';');
result.TrackArtist = file.Tag.OriginalArtist;
result.TrackArtistRaw = file.Tag.OriginalArtist;
result.AudioBitrate = file.Bitrate;
result.AudioChannels = file.AudioMode.HasValue ? (int?)file.AudioMode.Value : null;
result.AudioSampleRate = file.Frequency;
result.Disk = tpos != null ? SafeParser.ToNumber<int?>(tpos.Text) : null;
result.Images = pics.Select(x => new AudioMetaDataImage
{
Data = x.Data,
Description = x.Description,
MimeType = x.MimeType,
Type = (AudioMetaDataImageType)x.PictureType
}).ToArray();
result.Time = file.Duration;
result.Title = file.Tag.Title.ToTitleCase(false);
result.TotalTrackNumbers = file.Tag.TrackCount;
result.TrackNumber = file.Tag.TrackNumber;
result.Year = file.Tag.Year;
// TODO
//var file = LiteFile.LoadFromFile(fileName);
//var tpos = file.Tag.FindFirstFrameById(FrameId.TPOS);
//Picture[] pics = file.Tag.FindFramesById(FrameId.APIC).Select(f => f.GetPicture()).ToArray();
//result.Release = file.Tag.Album;
//result.Artist = file.Tag.Artist;
//result.ArtistRaw = file.Tag.Artist;
//result.Genres = (file.Tag.Genre ?? string.Empty).Split(';');
//result.TrackArtist = file.Tag.OriginalArtist;
//result.TrackArtistRaw = file.Tag.OriginalArtist;
//result.AudioBitrate = file.Bitrate;
//result.AudioChannels = file.AudioMode.HasValue ? (int?)file.AudioMode.Value : null;
//result.AudioSampleRate = file.Frequency;
//result.Disk = tpos != null ? SafeParser.ToNumber<int?>(tpos.Text) : null;
//result.Images = pics.Select(x => new AudioMetaDataImage
//{
// Data = x.Data,
// Description = x.Description,
// MimeType = x.MimeType,
// Type = (AudioMetaDataImageType)x.PictureType
//}).ToArray();
//result.Time = file.Duration;
//result.Title = file.Tag.Title.ToTitleCase(false);
//result.TotalTrackNumbers = file.Tag.TrackCount;
//result.TrackNumber = file.Tag.TrackNumber;
//result.Year = file.Tag.Year;
isSuccess = true;
}
catch (Exception ex)
@ -157,29 +160,30 @@ namespace Roadie.Library.MetaData.ID3Tags
var isSuccess = false;
try
{
var tagFile = TagLib.File.Create(fileName);
result.Release = tagFile.Tag.Album;
result.Artist = !string.IsNullOrEmpty(tagFile.Tag.JoinedAlbumArtists) ? tagFile.Tag.JoinedAlbumArtists : tagFile.Tag.JoinedPerformers;
result.ArtistRaw = !string.IsNullOrEmpty(tagFile.Tag.JoinedAlbumArtists) ? tagFile.Tag.JoinedAlbumArtists : tagFile.Tag.JoinedPerformers;
result.Genres = tagFile.Tag.Genres != null ? tagFile.Tag.Genres : new string[0];
result.TrackArtist = tagFile.Tag.JoinedPerformers;
result.TrackArtistRaw = tagFile.Tag.JoinedPerformers;
result.AudioBitrate = (tagFile.Properties.AudioBitrate > 0 ? (int?)tagFile.Properties.AudioBitrate : null);
result.AudioChannels = (tagFile.Properties.AudioChannels > 0 ? (int?)tagFile.Properties.AudioChannels : null);
result.AudioSampleRate = (tagFile.Properties.AudioSampleRate > 0 ? (int?)tagFile.Properties.AudioSampleRate : null);
result.Disk = (tagFile.Tag.Disc > 0 ? (int?)tagFile.Tag.Disc : null);
result.Images = (tagFile.Tag.Pictures != null ? tagFile.Tag.Pictures.Select(x => new AudioMetaDataImage
{
Data = x.Data.Data,
Description = x.Description,
MimeType = x.MimeType,
Type = (AudioMetaDataImageType)x.Type
}).ToArray() : null);
result.Time = (tagFile.Properties.Duration.TotalMinutes > 0 ? (TimeSpan?)tagFile.Properties.Duration : null);
result.Title = tagFile.Tag.Title.ToTitleCase(false);
result.TotalTrackNumbers = (tagFile.Tag.TrackCount > 0 ? (int?)tagFile.Tag.TrackCount : null);
result.TrackNumber = (tagFile.Tag.Track > 0 ? (short?)tagFile.Tag.Track : null);
result.Year = (tagFile.Tag.Year > 0 ? (int?)tagFile.Tag.Year : null);
// TODO
//var tagFile = TagLib.File.Create(fileName);
//result.Release = tagFile.Tag.Album;
//result.Artist = !string.IsNullOrEmpty(tagFile.Tag.JoinedAlbumArtists) ? tagFile.Tag.JoinedAlbumArtists : tagFile.Tag.JoinedPerformers;
//result.ArtistRaw = !string.IsNullOrEmpty(tagFile.Tag.JoinedAlbumArtists) ? tagFile.Tag.JoinedAlbumArtists : tagFile.Tag.JoinedPerformers;
//result.Genres = tagFile.Tag.Genres != null ? tagFile.Tag.Genres : new string[0];
//result.TrackArtist = tagFile.Tag.JoinedPerformers;
//result.TrackArtistRaw = tagFile.Tag.JoinedPerformers;
//result.AudioBitrate = (tagFile.Properties.AudioBitrate > 0 ? (int?)tagFile.Properties.AudioBitrate : null);
//result.AudioChannels = (tagFile.Properties.AudioChannels > 0 ? (int?)tagFile.Properties.AudioChannels : null);
//result.AudioSampleRate = (tagFile.Properties.AudioSampleRate > 0 ? (int?)tagFile.Properties.AudioSampleRate : null);
//result.Disk = (tagFile.Tag.Disc > 0 ? (int?)tagFile.Tag.Disc : null);
//result.Images = (tagFile.Tag.Pictures != null ? tagFile.Tag.Pictures.Select(x => new AudioMetaDataImage
//{
// Data = x.Data.Data,
// Description = x.Description,
// MimeType = x.MimeType,
// Type = (AudioMetaDataImageType)x.Type
//}).ToArray() : null);
//result.Time = (tagFile.Properties.Duration.TotalMinutes > 0 ? (TimeSpan?)tagFile.Properties.Duration : null);
//result.Title = tagFile.Tag.Title.ToTitleCase(false);
//result.TotalTrackNumbers = (tagFile.Tag.TrackCount > 0 ? (int?)tagFile.Tag.TrackCount : null);
//result.TrackNumber = (tagFile.Tag.Track > 0 ? (short?)tagFile.Tag.Track : null);
//result.Year = (tagFile.Tag.Year > 0 ? (int?)tagFile.Tag.Year : null);
isSuccess = true;
}
catch (Exception ex)