diff --git a/RoadieApi/Controllers/SubsonicController.cs b/RoadieApi/Controllers/SubsonicController.cs index 927db64..7d5ed2b 100644 --- a/RoadieApi/Controllers/SubsonicController.cs +++ b/RoadieApi/Controllers/SubsonicController.cs @@ -7,6 +7,7 @@ using Roadie.Api.Services; using Roadie.Library.Caching; using Roadie.Library.Identity; using Roadie.Library.Models.ThirdPartyApi.Subsonic; +using System.Net; using System.Threading.Tasks; namespace Roadie.Api.Controllers @@ -42,6 +43,15 @@ namespace Roadie.Api.Controllers return this.BuildResponse(request, result.Data, "musicFolders"); } + [HttpGet("getMusicDirectory.view")] + [ProducesResponseType(200)] + public async Task GetMusicDirectory([FromQuery]Request request, string id) + { + var result = await this.SubsonicService.GetMusicDirectory(request, null, id); + return this.BuildResponse(request, result.Data, "directory"); + } + + [HttpGet("getPlaylists.view")] [ProducesResponseType(200)] public async Task GetPlaylists([FromQuery]Request request, string username) @@ -50,6 +60,43 @@ namespace Roadie.Api.Controllers return this.BuildResponse(request, result.Data, "playlists"); } + [HttpGet("getGenres.view")] + [ProducesResponseType(200)] + public async Task GetGenres([FromQuery]Request request) + { + var result = await this.SubsonicService.GetGenres(request); + return this.BuildResponse(request, result.Data, "genres"); + } + + [HttpGet("getPodcasts.view")] + [ProducesResponseType(200)] + public async Task GetPodcasts([FromQuery]Request request, bool includeEpisodes) + { + var result = await this.SubsonicService.GetPodcasts(request); + return this.BuildResponse(request, result.Data, "podcasts"); + } + + [HttpGet("getCoverArt.view")] + [ProducesResponseType(200)] + public async Task GetCoverArt([FromQuery]Request request, int? size) + { + var result = await this.SubsonicService.GetCoverArt(request, size); + if (result == null || result.IsNotFoundResult) + { + return NotFound(); + } + if (!result.IsSuccess) + { + return StatusCode((int)HttpStatusCode.InternalServerError); + } + return File(fileContents: result.Data.Bytes, + contentType: result.ContentType, + fileDownloadName: $"{ result.Data.Caption ?? request.id.ToString()}.jpg", + lastModified: result.LastModified, + entityTag: result.ETag); + } + + [HttpGet("ping.view")] [HttpPost("ping.view")] [ProducesResponseType(200)] diff --git a/RoadieApi/Services/ISubsonicService.cs b/RoadieApi/Services/ISubsonicService.cs index 2a9685a..0bdd8af 100644 --- a/RoadieApi/Services/ISubsonicService.cs +++ b/RoadieApi/Services/ISubsonicService.cs @@ -16,6 +16,10 @@ namespace Roadie.Api.Services Task> GetPodcasts(Request request); + Task> GetMusicDirectory(Request request, Roadie.Library.Models.Users.User roadieUser, string id); + + Task> GetCoverArt(Request request, int? size); + OperationResult Ping(Request request); } } \ No newline at end of file diff --git a/RoadieApi/Services/PlaylistService.cs b/RoadieApi/Services/PlaylistService.cs index cbbb64f..ffc334b 100644 --- a/RoadieApi/Services/PlaylistService.cs +++ b/RoadieApi/Services/PlaylistService.cs @@ -60,7 +60,7 @@ namespace Roadie.Api.Services }, User = new DataToken { - Text = u.Username, + Text = u.UserName, Value = u.RoadieId.ToString() }, PlaylistCount = this.DbContext.PlaylistTracks.Where(x => x.PlayListId == pl.Id).Count(), diff --git a/RoadieApi/Services/SubsonicService.cs b/RoadieApi/Services/SubsonicService.cs index 4951099..0863c2c 100644 --- a/RoadieApi/Services/SubsonicService.cs +++ b/RoadieApi/Services/SubsonicService.cs @@ -1,17 +1,10 @@ -using Mapster; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Roadie.Library; using Roadie.Library.Caching; using Roadie.Library.Configuration; using Roadie.Library.Encoding; -using Roadie.Library.Enums; using Roadie.Library.Extensions; -using Roadie.Library.Models; -using Roadie.Library.Models.Pagination; -using Roadie.Library.Models.Releases; -using Roadie.Library.Models.Statistics; -using subsonic = Roadie.Library.Models.ThirdPartyApi.Subsonic; +using Roadie.Library.Imaging; using Roadie.Library.Models.Users; using Roadie.Library.Utility; using System; @@ -21,6 +14,7 @@ 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 { @@ -32,8 +26,6 @@ namespace Roadie.Api.Services /// public class SubsonicService : ServiceBase, ISubsonicService { - public const string ArtistIdIdentifier = "A:"; - public const string CollectionIdentifier = "C:"; public const string SubsonicVersion = "1.16.1"; public SubsonicService(IRoadieSettings configuration, @@ -58,7 +50,7 @@ namespace Roadie.Api.Services version = SubsonicService.SubsonicVersion, status = subsonic.ResponseStatus.ok } - }; + }; } /// @@ -104,12 +96,12 @@ namespace Roadie.Api.Services /// If specified, only return a result if the artist collection has changed since the given time (in milliseconds since 1 Jan 1970). public async Task> GetIndexes(subsonic.Request request, string musicFolderId = null, long? ifModifiedSince = null) { - + var modifiedSinceFilter = ifModifiedSince.HasValue ? (DateTime?)ifModifiedSince.Value.FromUnixTime() : null; - subsonic.MusicFolder musicFolderFilter = string.IsNullOrEmpty(musicFolderId) ? null : this.MusicFolders().FirstOrDefault(x => x.id == SafeParser.ToNumber(musicFolderId)); + subsonic.MusicFolder musicFolderFilter = string.IsNullOrEmpty(musicFolderId) ? new subsonic.MusicFolder() : this.MusicFolders().FirstOrDefault(x => x.id == SafeParser.ToNumber(musicFolderId)); var indexes = new List(); - if (musicFolderFilter == this.CollectionMusicFolder()) + if (musicFolderFilter.id == this.CollectionMusicFolder().id) { // Collections for Music Folders by Alpha First foreach (var collectionFirstLetter in (from c in this.DbContext.Collections @@ -126,7 +118,7 @@ namespace Roadie.Api.Services orderby c.SortName, c.Name select new subsonic.Artist { - id = SubsonicService.CollectionIdentifier + c.RoadieId.ToString(), + id = subsonic.Request.CollectionIdentifier + c.RoadieId.ToString(), name = c.Name, artistImageUrl = this.MakeCollectionThumbnailImage(c.RoadieId).Url, averageRating = 0, @@ -152,7 +144,7 @@ namespace Roadie.Api.Services orderby c.SortName, c.Name select new subsonic.Artist { - id = SubsonicService.ArtistIdIdentifier + c.RoadieId.ToString(), + id = subsonic.Request.ArtistIdIdentifier + c.RoadieId.ToString(), name = c.Name, artistImageUrl = this.MakeArtistThumbnailImage(c.RoadieId).Url, averageRating = 0, @@ -194,7 +186,7 @@ namespace Roadie.Api.Services id = playlist.RoadieId.ToString(), name = playlist.Name, comment = playlist.Description, - owner = u.Username, + owner = u.UserName, songCount = trackCount, duration = playListDuration ?? 0, created = playlist.CreatedDate, @@ -223,7 +215,36 @@ namespace Roadie.Api.Services public async Task> GetGenres(subsonic.Request request) { - throw new NotImplementedException(); + 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 OperationResult + { + IsSuccess = true, + Data = new subsonic.Response + { + version = SubsonicService.SubsonicVersion, + status = subsonic.ResponseStatus.ok, + ItemElementName = subsonic.ItemChoiceType.genres, + Item = new subsonic.Genres + { + genre = genres.ToArray() + } + } + }; } public async Task> GetPodcasts(subsonic.Request request) @@ -241,5 +262,251 @@ namespace Roadie.Api.Services }; } + /// + /// 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. + /// + /// Query from application. + /// A string which uniquely identifies the music folder. Obtained by calls to getIndexes or getMusicDirectory. + /// + public async Task> GetMusicDirectory(subsonic.Request request, User roadieUser, string id) + { + var directory = new subsonic.Directory(); + var user = this.GetUser(roadieUser?.UserId); + + // Request to get albums for an Artist + if (request.ArtistId != null) + { + var artist = this.GetArtist(request.ArtistId.Value); + if (artist == null) + { + return new OperationResult(true, $"Invalid ArtistId [{ request.ArtistId}]"); + } + directory.id = subsonic.Request.ArtistIdIdentifier + artist.RoadieId.ToString(); + directory.name = artist.Name; + var artistRating = user?.ArtistRatings?.FirstOrDefault(x => x.ArtistId == artist.Id); + directory.starred = artistRating != null ? (DateTime?)(artistRating.LastUpdated ?? artistRating.CreatedDate) : null; + directory.child = (from r in this.DbContext.Releases + join ur in this.DbContext.UserReleases on r.Id equals ur.ReleaseId into urr + from ur in urr.DefaultIfEmpty() + let genre = r.Genres.FirstOrDefault() + where r.ArtistId == artist.Id + select new subsonic.Child + { + id = subsonic.Request.ReleaseIdIdentifier + r.RoadieId.ToString(), + parent = subsonic.Request.ArtistIdIdentifier + artist.RoadieId.ToString(), + isDir = true, + title = r.Title, + album = r.Title, + albumId = subsonic.Request.ReleaseIdIdentifier + r.RoadieId.ToString(), + artist = artist.Name, + year = (r.ReleaseDate ?? r.CreatedDate).Year, + genre = genre != null ? genre.Genre.Name : null, + coverArt = subsonic.Request.ReleaseIdIdentifier + r.RoadieId.ToString(), + averageRating = artist.Rating ?? 0, + created = artist.CreatedDate, + playCount = (from ut in this.DbContext.UserTracks + join t in this.DbContext.Tracks on ut.TrackId equals t.Id + join rm in this.DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id + where rm.ReleaseId == r.Id + where ut.PlayedCount != null + select ut.PlayedCount ?? 0).Sum(), + starred = ur != null ? (ur.IsFavorite ?? false ? (DateTime?)(ur.LastUpdated ?? ur.CreatedDate) : null) : null, + }).ToArray(); + } + // Request to get albums for in a Collection + else if (request.CollectionId != null) + { + var collection = this.GetCollection(request.CollectionId.Value); + if (collection == null) + { + return new OperationResult(true, $"Invalid CollectionId [{ request.CollectionId}]"); + } + directory.id = subsonic.Request.CollectionIdentifier + collection.RoadieId.ToString(); + directory.name = collection.Name; + directory.child = (from cr in this.DbContext.CollectionReleases + join r in this.DbContext.Releases on cr.ReleaseId equals r.Id + join a in this.DbContext.Artists on r.ArtistId equals a.Id + join ur in this.DbContext.UserReleases on r.Id equals ur.ReleaseId into urr + from ur in urr.DefaultIfEmpty() + let genre = r.Genres.FirstOrDefault() + let playCount = (from ut in this.DbContext.UserTracks + join t in this.DbContext.Tracks on ut.TrackId equals t.Id + join rm in this.DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id + where rm.ReleaseId == r.Id + select ut.PlayedCount).Sum() + where cr.CollectionId == collection.Id + select new subsonic.Child + { + id = subsonic.Request.ReleaseIdIdentifier + r.RoadieId.ToString(), + parent = subsonic.Request.CollectionIdentifier + collection.RoadieId.ToString(), + isDir = true, + title = r.Title, + album = r.Title, + albumId = subsonic.Request.ReleaseIdIdentifier + r.RoadieId.ToString(), + artist = a.Name, + year = (r.ReleaseDate ?? r.CreatedDate).Year, + genre = genre != null ? genre.Genre.Name : null, + coverArt = subsonic.Request.ReleaseIdIdentifier + r.RoadieId.ToString(), + created = collection.CreatedDate, + playCount = playCount ?? 0 + }).ToArray(); + + } + // Request to get Tracks for an Album + else + { + var release = this.GetRelease(request.ReleaseId.Value); + if (release == null) + { + return new OperationResult(true, $"Invalid ReleaseId [{ request.ReleaseId}]"); + } + 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(); + directory.starred = releaseRating != null ? (releaseRating.IsFavorite ?? false ? (DateTime?)(releaseRating.LastUpdated ?? releaseRating.CreatedDate) : null) : null; + var releaseTracks = release.Medias.SelectMany(x => x.Tracks); + var genre = release.Genres.FirstOrDefault(); + directory.child = (from t in releaseTracks + join ut in this.DbContext.UserTracks on t.Id equals ut.TrackId into utg + from ut in utg.DefaultIfEmpty() + join rm in this.DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id + let playCount = (from ut in this.DbContext.UserTracks + where ut.TrackId == t.Id + select ut.PlayedCount).Sum() + select new subsonic.Child + { + id = subsonic.Request.TrackIdIdentifier + t.RoadieId.ToString(), + album = release.Title, + albumId = subsonic.Request.ReleaseIdIdentifier + release.RoadieId.ToString(), + artist = release.Artist.Name, + artistId = subsonic.Request.ArtistIdIdentifier + release.Artist.RoadieId.ToString(), + averageRating = release.Rating ?? 0, + averageRatingSpecified= true, + bitRate = 320, + bitRateSpecified = true, + contentType = "audio/mpeg", + coverArt = subsonic.Request.ReleaseIdIdentifier + release.RoadieId.ToString(), + created = release.CreatedDate, + createdSpecified = true, + discNumber = rm.MediaNumber, + discNumberSpecified = true, + duration = t.Duration.ToSecondsFromMilliseconds(), + durationSpecified = true, + genre = genre != null ? genre.Genre.Name : null, + parent = subsonic.Request.ReleaseIdIdentifier + release.RoadieId.ToString(), + path = $"{ release.Artist.Name}/{ release.Title}/{ t.TrackNumber } - { t.Title }.mp3", + playCountSpecified = true, + size = t.FileSize ?? 0, + sizeSpecified = true, + starred = ut != null ? (ut.IsFavorite ?? false ? (DateTime?)(ut.LastUpdated ?? ut.CreatedDate) : null) : null, + starredSpecified = ut != null, + suffix = "mp3", + title = t.Title, + track = t.TrackNumber, + trackSpecified = true, + type = subsonic.MediaType.music, + typeSpecified = true, + userRating = ut != null ? ut.Rating : 0, + userRatingSpecified = ut != null, + year = (release.ReleaseDate ?? release.CreatedDate).Year, + yearSpecified = true, + playCount = (from ut in this.DbContext.UserTracks + join t in this.DbContext.Tracks on ut.TrackId equals t.Id + join rm in this.DbContext.ReleaseMedias on t.ReleaseMediaId equals rm.Id + where rm.ReleaseId == release.Id + where ut.PlayedCount != null + select ut.PlayedCount ?? 0).Sum(), + }).ToArray(); + directory.playCount = directory.child.Select(x => x.playCount).Sum(); + } + return new OperationResult + { + IsSuccess = true, + Data = new subsonic.Response + { + version = SubsonicService.SubsonicVersion, + status = subsonic.ResponseStatus.ok, + ItemElementName = subsonic.ItemChoiceType.directory, + Item = directory + } + }; + } + + public async Task> GetCoverArt(subsonic.Request request, int? size) + { + var sw = Stopwatch.StartNew(); + var result = new FileOperationResult + { + Data = new Roadie.Library.Models.Image() + }; + + if (request.ArtistId != null) + { + var artist = this.GetArtist(request.ArtistId.Value); + if (artist == null) + { + return new FileOperationResult(true, $"Invalid ArtistId [{ request.ArtistId}]"); + } + result.Data.Bytes = artist.Thumbnail; + } + else if(request.CollectionId != null) + { + var collection = this.GetCollection(request.CollectionId.Value); + if (collection == null) + { + return new FileOperationResult(true, $"Invalid CollectionId [{ request.CollectionId}]"); + } + result.Data.Bytes = collection.Thumbnail; + } + else if(request.ReleaseId != null) + { + var release = this.GetRelease(request.ReleaseId.Value); + if (release == null) + { + return new FileOperationResult(true, $"Invalid ReleaseId [{ request.ReleaseId}]"); + } + result.Data.Bytes = release.Thumbnail; + } + else if (request.PlaylistId != null) + { + var playlist = this.GetPlaylist(request.PlaylistId.Value); + if (playlist == null) + { + return new FileOperationResult(true, $"Invalid PlaylistId [{ request.PlaylistId}]"); + } + result.Data.Bytes = playlist.Thumbnail; + } + else if(!string.IsNullOrEmpty(request.u)) + { + var userByUsername = this.DbContext.Users.FirstOrDefault(x => x.UserName == request.u); + if(userByUsername == null) + { + return new FileOperationResult(true, $"Invalid Username [{ request.u}]"); + } + result.Data.Bytes = userByUsername.Avatar; + } + + if (size.HasValue && result.Data.Bytes != null) + { + result.Data.Bytes = ImageHelper.ResizeImage(result.Data.Bytes, size.Value, size.Value); + result.ETag = EtagHelper.GenerateETag(this.HttpEncoder, result.Data.Bytes); + result.LastModified = DateTime.UtcNow; + } + result.IsSuccess = true; + sw.Stop(); + return new FileOperationResult(result.Messages) + { + Data = result.Data, + ETag = result.ETag, + LastModified = result.LastModified, + ContentType = "image/jpeg", + Errors = result?.Errors, + IsSuccess = result?.IsSuccess ?? false, + OperationTime = sw.ElapsedMilliseconds + }; + } + } } \ No newline at end of file diff --git a/RoadieApi/Services/UserService.cs b/RoadieApi/Services/UserService.cs index 1ade803..d203e64 100644 --- a/RoadieApi/Services/UserService.cs +++ b/RoadieApi/Services/UserService.cs @@ -46,7 +46,7 @@ namespace Roadie.Api.Services } var result = (from u in this.DbContext.Users - where (request.FilterValue.Length == 0 || (request.FilterValue.Length > 0 && (u.Username.Contains(request.FilterValue)))) + where (request.FilterValue.Length == 0 || (request.FilterValue.Length > 0 && (u.UserName.Contains(request.FilterValue)))) select new UserList { DatabaseId = u.Id, diff --git a/RoadieLibrary/Extensions/IntEx.cs b/RoadieLibrary/Extensions/IntEx.cs index 6e2320e..3b7a7b1 100644 --- a/RoadieLibrary/Extensions/IntEx.cs +++ b/RoadieLibrary/Extensions/IntEx.cs @@ -1,4 +1,6 @@ -namespace Roadie.Library.Extensions +using System; + +namespace Roadie.Library.Extensions { public static class IntEx { @@ -10,5 +12,15 @@ } return value.HasValue ? value : alternative; } + + public static int ToSecondsFromMilliseconds(this int? value) + { + if (value > 0) + { + var contentDurationTimeSpan = TimeSpan.FromMilliseconds((double)(value ?? 0)); + return (int)contentDurationTimeSpan.TotalSeconds; + } + return 0; + } } } \ No newline at end of file diff --git a/RoadieLibrary/Identity/ApplicationUser.cs b/RoadieLibrary/Identity/ApplicationUser.cs index 83efb96..fd35003 100644 --- a/RoadieLibrary/Identity/ApplicationUser.cs +++ b/RoadieLibrary/Identity/ApplicationUser.cs @@ -120,10 +120,10 @@ namespace Roadie.Library.Identity public ICollection TrackRatings { get; set; } - [Column("username")] - [Required] - [StringLength(20)] - public string Username { get; set; } + //[Column("username")] + //[Required] + //[StringLength(20)] + //public string UserName { get; set; } public virtual ICollection UserRoles { get; set; } diff --git a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs index 5476c80..2ca0957 100644 --- a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs +++ b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs @@ -1,4 +1,5 @@ using Roadie.Library.Extensions; +using Roadie.Library.Utility; using System; namespace Roadie.Library.Models.ThirdPartyApi.Subsonic @@ -6,6 +7,12 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic [Serializable] public class Request { + public const string ArtistIdIdentifier = "A:"; + public const string CollectionIdentifier = "C:"; + public const string ReleaseIdIdentifier = "R:"; + public const string TrackIdIdentifier = "T:"; + public const string PlaylistdIdentifier = "P:"; + /// /// A unique string identifying the client application. /// @@ -83,6 +90,77 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic /// public string v { get; set; } + /// + /// A string which uniquely identifies the music folder. Obtained by calls to getIndexes or getMusicDirectory. + /// + public string id { get; set; } + + public Guid? ArtistId + { + get + { + if(string.IsNullOrEmpty(this.id)) + { + return null; + } + if(this.id.StartsWith(Request.ArtistIdIdentifier)) + { + return SafeParser.ToGuid(this.id.Replace(Request.ArtistIdIdentifier, "")); + } + return null; + } + } + + public Guid? CollectionId + { + get + { + if (string.IsNullOrEmpty(this.id)) + { + return null; + } + if (this.id.StartsWith(Request.CollectionIdentifier)) + { + return SafeParser.ToGuid(this.id.Replace(Request.CollectionIdentifier, "")); + } + return null; + } + } + + public Guid? ReleaseId + { + get + { + if (string.IsNullOrEmpty(this.id)) + { + return null; + } + if (this.id.StartsWith(Request.ReleaseIdIdentifier)) + { + return SafeParser.ToGuid(this.id.Replace(Request.ReleaseIdIdentifier, "")); + } + return null; + } + } + + public Guid? PlaylistId + { + get + { + if (string.IsNullOrEmpty(this.id)) + { + return null; + } + if (this.id.StartsWith(Request.PlaylistdIdentifier)) + { + return SafeParser.ToGuid(this.id.Replace(Request.PlaylistdIdentifier, "")); + } + return null; + } + } + + + //public user CheckPasswordGetUser(ICacheManager cacheManager, RoadieDbContext context) //{ // user user = null; diff --git a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/subsonic-rest-api-1_16_1.cs b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/subsonic-rest-api-1_16_1.cs index 6583ac8..4a50772 100644 --- a/RoadieLibrary/Models/ThirdPartyApi/Subsonic/subsonic-rest-api-1_16_1.cs +++ b/RoadieLibrary/Models/ThirdPartyApi/Subsonic/subsonic-rest-api-1_16_1.cs @@ -161,7 +161,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic /// [System.Xml.Serialization.XmlAttributeAttribute()] - public long size; + public int size; /// [System.Xml.Serialization.XmlIgnoreAttribute()] @@ -221,7 +221,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic /// [System.Xml.Serialization.XmlAttributeAttribute()] - public double averageRating; + public short averageRating; /// [System.Xml.Serialization.XmlIgnoreAttribute()] @@ -229,7 +229,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic /// [System.Xml.Serialization.XmlAttributeAttribute()] - public long playCount; + public int playCount; /// [System.Xml.Serialization.XmlIgnoreAttribute()] @@ -253,7 +253,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic /// [System.Xml.Serialization.XmlAttributeAttribute()] - public System.DateTime starred; + public System.DateTime? starred; /// [System.Xml.Serialization.XmlIgnoreAttribute()] @@ -1688,7 +1688,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic /// [System.Xml.Serialization.XmlTextAttribute()] - public string[] Text; + public string value; } /// @@ -1718,7 +1718,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic /// [System.Xml.Serialization.XmlAttributeAttribute()] - public System.DateTime starred; + public System.DateTime? starred; /// [System.Xml.Serialization.XmlIgnoreAttribute()] @@ -1734,7 +1734,7 @@ namespace Roadie.Library.Models.ThirdPartyApi.Subsonic /// [System.Xml.Serialization.XmlAttributeAttribute()] - public double averageRating; + public short averageRating; /// [System.Xml.Serialization.XmlIgnoreAttribute()] diff --git a/RoadieLibrary/Utility/SafeParser.cs b/RoadieLibrary/Utility/SafeParser.cs index 2db2f6c..87d07ee 100644 --- a/RoadieLibrary/Utility/SafeParser.cs +++ b/RoadieLibrary/Utility/SafeParser.cs @@ -59,6 +59,20 @@ namespace Roadie.Library.Utility } } + public static Guid? ToGuid(object input) + { + if(input == null) + { + return null; + } + if (!Guid.TryParse(input.ToString(), out Guid result)) + { + return null; + } + return result; + } + + public static DateTime? ToDateTime(object input) { if (input == null)