From 3a6e0f64bc744c2e1194492b3cd5e99a0efab48a Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Wed, 29 Dec 2021 14:06:52 -0500 Subject: [PATCH] New endpoint New endpoint to upload song with metadata and cover art as separate entities --- Controllers/Managers/CoverArtManager.cs | 32 ++++++++- Controllers/Managers/SongManager.cs | 81 +++++++++++++++++----- Controllers/Utilities/MetadataRetriever.cs | 68 ++++++++++++++++-- Controllers/v1/SongDataController.cs | 51 ++++++++++++++ Icarus.csproj | 1 + Icarus.sln | 14 ---- Models/Song.cs | 6 ++ Scripts/MySQL/create_database.sql | 3 + Scripts/MySQL/delete_all.sql | 1 - Startup.cs | 4 ++ 10 files changed, 221 insertions(+), 40 deletions(-) delete mode 100644 Icarus.sln diff --git a/Controllers/Managers/CoverArtManager.cs b/Controllers/Managers/CoverArtManager.cs index 39bb612..d6ac8b1 100644 --- a/Controllers/Managers/CoverArtManager.cs +++ b/Controllers/Managers/CoverArtManager.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.Configuration; using System.IO; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + using Icarus.Constants; using Icarus.Controllers.Utilities; using Icarus.Database.Contexts; @@ -118,6 +120,34 @@ namespace Icarus.Controllers.Managers return null; } + public CoverArt SaveCoverArt(IFormFile data, Song song) + { + var cover = new CoverArt { SongTitle = song.Title }; + + try + { + var dirMgr = new DirectoryManager(_rootCoverArtPath); + var defaultExtension = ".png"; + dirMgr.CreateDirectory(song); + + var segment = cover.GenerateFilename(0); + var imagePath = dirMgr.SongDirectory + segment + defaultExtension; + + cover.ImagePath = imagePath; + + using (var fileStream = new FileStream(imagePath, FileMode.Create)) + { + data.CopyTo(fileStream); + } + } + catch (Exception ex) + { + _logger.Error(ex.Message, "An error occurred"); + } + + return cover; + } + public CoverArt GetCoverArt(Song song) { return _coverArtContext.CoverArtImages.FirstOrDefault(cov => cov.SongTitle.Equals(song.Title)); diff --git a/Controllers/Managers/SongManager.cs b/Controllers/Managers/SongManager.cs index 69b5f62..a4b8e64 100644 --- a/Controllers/Managers/SongManager.cs +++ b/Controllers/Managers/SongManager.cs @@ -176,27 +176,24 @@ namespace Icarus.Controllers.Managers _logger.Info("Starting the process of saving the song to the filesystem"); var song = await SaveSongTemp(songFile); - System.IO.File.Delete(song.SongPath()); DirectoryManager dirMgr = new DirectoryManager(_config, song); dirMgr.CreateDirectory(); - var fileDir = dirMgr.SongDirectory; - var filePath = fileDir; - var songFilename = song.Filename; - - if (!songFilename.EndsWith(".mp3")) - filePath += $"{songFilename}.mp3"; - else - filePath += $"{songFilename}"; + var filePath = dirMgr.SongDirectory; + filePath += $"{song.Filename}"; _logger.Info($"Absolute song path: {filePath}"); using (var fileStream = new FileStream(filePath, FileMode.Create)) { - await (songFile.CopyToAsync(fileStream)); - song.SongDirectory = fileDir; + var songBytes = await System.IO.File.ReadAllBytesAsync(song.SongPath()); + + System.IO.File.WriteAllBytesAsync(filePath, songBytes); + System.IO.File.Delete(song.SongPath()); + + song.SongDirectory = dirMgr.SongDirectory; _logger.Info("Song successfully saved to filesystem"); } @@ -214,6 +211,55 @@ namespace Icarus.Controllers.Managers } } + public async Task SaveSongToFileSystem(IFormFile songFile, IFormFile coverArtData, Song song) + { + if (string.IsNullOrEmpty(song.Filename)) + { + song.Filename = song.GenerateFilename(1); + } + + song.SongDirectory = _tempDirectoryRoot; + song.DateCreated = DateTime.Now; + + using (var filestream = new FileStream(song.SongPath(), FileMode.Create)) + { + _logger.Info("Saving song to temporary directory"); + await songFile.CopyToAsync(filestream); + } + + var coverMgr = new CoverArtManager(_config); + var coverArt = coverMgr.SaveCoverArt(coverArtData, song); + + var meta = new MetadataRetriever(); + song.Duration = meta.RetrieveSongDuration(song.SongPath()); + // const int trackCount = meta.RetrieveTrackCount(song.) + meta.UpdateMetadata(song, song); + meta.UpdateCoverArt(song, coverArt); + + DirectoryManager dirMgr = new DirectoryManager(_config, song); + dirMgr.CreateDirectory(); + + var filePath = dirMgr.SongDirectory + song.Filename; + + _logger.Info($"Absolute song path: {filePath}"); + + + using (var fileStream = new FileStream(filePath, FileMode.Create)) + { + var songBytes = await System.IO.File.ReadAllBytesAsync(song.SongPath()); + + await System.IO.File.WriteAllBytesAsync(filePath, songBytes); + System.IO.File.Delete(song.SongPath()); + song.SongDirectory = dirMgr.SongDirectory; + + _logger.Info("Song successfully saved to filesystem"); + } + + + coverMgr.SaveCoverArtToDatabase(ref song, ref coverArt); + SaveSongToDatabase(song); + } + public async Task RetrieveSong(Song songMetaData) @@ -248,22 +294,21 @@ namespace Icarus.Controllers.Managers private async Task SaveSongTemp(IFormFile songFile) { var song = new Song(); - var filename = song.GenerateFilename(); - var filePath = Path.Combine(_tempDirectoryRoot, filename); + _logger.Info("Assigning song filename"); + song.SongDirectory = _tempDirectoryRoot; + song.Filename = song.GenerateFilename(1); - - using (var filestream = new FileStream(filePath, FileMode.Create)) + using (var filestream = new FileStream(song.SongPath(), FileMode.Create)) { _logger.Info("Saving song to temporary directory"); await songFile.CopyToAsync(filestream); } MetadataRetriever meta = new MetadataRetriever(); - song = meta.RetrieveMetaData(filePath); + song = meta.RetrieveMetaData(song.SongPath()); - _logger.Info("Assigning song filename"); - song.Filename = filename; song.SongDirectory = _tempDirectoryRoot; + song.Filename = song.GenerateFilename(1); song.DateCreated = DateTime.Now; return song; diff --git a/Controllers/Utilities/MetadataRetriever.cs b/Controllers/Utilities/MetadataRetriever.cs index a689b0b..d33e89b 100644 --- a/Controllers/Utilities/MetadataRetriever.cs +++ b/Controllers/Utilities/MetadataRetriever.cs @@ -81,13 +81,21 @@ namespace Icarus.Controllers.Utilities _genre = string.Join("", fileTag.Tag.Genres); _year = (int)fileTag.Tag.Year; _duration = (int)fileTag.Properties.Duration.TotalSeconds; + var albumArtist = string.Join("", fileTag.Tag.AlbumArtists); + var track = (int)fileTag.Tag.Track; + var disc = (int)fileTag.Tag.Disc; song.Title = _title; song.Artist = _artist; song.AlbumTitle = _album; + song.AlbumArtist = albumArtist; song.Genre = _genre; song.Year = _year; song.Duration = _duration; + song.Track = track; + song.Disc = disc; + song.TrackCount = (int)fileTag.Tag.TrackCount; + song.DiscCount = (int)fileTag.Tag.DiscCount; } catch (Exception ex) { @@ -100,6 +108,15 @@ namespace Icarus.Controllers.Utilities return song; } + public int RetrieveSongDuration(string filepath) + { + var duration = 0; + var fileTag = TagLib.File.Create(filepath); + duration = (int)fileTag.Properties.Duration.TotalSeconds; + + return duration; + } + public byte[] RetrieveCoverArtBytes(Song song) { try @@ -162,6 +179,11 @@ namespace Icarus.Controllers.Utilities var album = updatedSong.AlbumTitle; var genre = updatedSong.Genre; var year = updatedSong.Year; + var albumArtist = updatedSong.AlbumArtist; + var track = updatedSong.Track; + var trackCount = updatedSong.TrackCount; + var disc = updatedSong.Disc; + var discCount = updatedSong.DiscCount; TagLib.File fileTag = TagLib.File.Create(filePath); try @@ -196,6 +218,26 @@ namespace Icarus.Controllers.Utilities _updatedSong.Year = year; fileTag.Tag.Year = (uint)year; break; + case "albumartist": + _updatedSong.AlbumArtist = albumArtist; + fileTag.Tag.AlbumArtists = new []{albumArtist}; + break; + case "track": + _updatedSong.Track = track; + fileTag.Tag.Track = (uint)(track); + break; + case "trackcount": + _updatedSong.TrackCount = trackCount; + fileTag.Tag.TrackCount = (uint)(trackCount); + break; + case "disc": + _updatedSong.Disc = disc; + fileTag.Tag.Disc = (uint)(disc); + break; + case "disccount": + _updatedSong.DiscCount = discCount; + fileTag.Tag.DiscCount = (uint)(discCount); + break; } } @@ -264,13 +306,13 @@ namespace Icarus.Controllers.Utilities songValues["Artists"] = String.IsNullOrEmpty(song.Artist); songValues["Album"] = String.IsNullOrEmpty(song.AlbumTitle); songValues["Genre"] = String.IsNullOrEmpty(song.Genre); + songValues["AlbumArtist"] = String.IsNullOrEmpty(song.AlbumArtist); - if (song.Year == null) - songValues["Year"] = true; - else if (song.Year == 0) - songValues["Year"] = true; - else - songValues["Year"] = false; + songValues["Year"] = CheckIntField(song.Year); + songValues["Track"] = CheckIntField(song.Track); + songValues["TrackCount"] = CheckIntField(song.TrackCount); + songValues["Disc"] = CheckIntField(song.Disc); + songValues["DiscCount"] = CheckIntField(song.Disc); Console.WriteLine("Checking for null data completed"); _logger.Info("Checking for null data completed"); @@ -284,6 +326,20 @@ namespace Icarus.Controllers.Utilities return songValues; } + + private bool CheckIntField(int? value) + { + if (value == null) + { + return true; + } + else if (value == 0) + { + return true; + } + + return false; + } #endregion } } diff --git a/Controllers/v1/SongDataController.cs b/Controllers/v1/SongDataController.cs index 091ff0d..9837390 100644 --- a/Controllers/v1/SongDataController.cs +++ b/Controllers/v1/SongDataController.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Icarus.Controllers.Managers; using Icarus.Models; @@ -60,6 +61,18 @@ namespace Icarus.Controllers.V1 return File(song.Data, "application/x-msdownload", songMetaData.Filename); } + // Assumes that the song already has metadata such as + // Title + // Artist + // Album + // Genre + // Year + // Track + // Track count + // Disc + // Disc count + // Cover art + // [HttpPost("upload"), DisableRequestSizeLimit] [Route("private-scoped")] [Authorize("upload:songs")] @@ -93,6 +106,33 @@ namespace Icarus.Controllers.V1 return NotFound(); } + // The client is expected to send the file, metadata, and cover art separately. + // Any metadata already on the file will be overwritten with values from the metadata + // as well as the cover art + // + [HttpPost("upload/with/data")] + [Route("private-scoped")] + [Authorize("upload:songs")] + public async Task Post ([FromForm] UploadSongWithDataForm up) + { + try + { + if (up.SongData.Length > 0 && up.CoverArtData.Length > 0 && !string.IsNullOrEmpty(up.SongFile)) + { + var song = Newtonsoft.Json.JsonConvert.DeserializeObject(up.SongFile); + _logger.LogInformation($"Song title: {song.Title}"); + + await _songMgr.SaveSongToFileSystem(up.SongData, up.CoverArtData, song); + } + } + catch (Exception ex) + { + _logger.LogError(ex.Message, "An error occurred"); + } + + return Ok(); + } + [HttpDelete("delete/{id}")] [Authorize("delete:songs")] public IActionResult Delete(int id) @@ -117,6 +157,17 @@ namespace Icarus.Controllers.V1 return Ok(); } + } + + public class UploadSongWithDataForm + { + [FromForm(Name = "file")] + public IFormFile SongData { get; set; } + [FromForm(Name = "cover")] + public IFormFile CoverArtData { get; set; } + [FromForm(Name = "metadata")] + public string SongFile { get; set; } + } } } diff --git a/Icarus.csproj b/Icarus.csproj index f0ea170..65d1413 100644 --- a/Icarus.csproj +++ b/Icarus.csproj @@ -11,6 +11,7 @@ + runtime; build; native; contentfiles; analyzers diff --git a/Icarus.sln b/Icarus.sln deleted file mode 100644 index b936964..0000000 --- a/Icarus.sln +++ /dev/null @@ -1,14 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/Models/Song.cs b/Models/Song.cs index bc6c9cb..fe7107b 100644 --- a/Models/Song.cs +++ b/Models/Song.cs @@ -18,6 +18,8 @@ namespace Icarus.Models public string AlbumTitle { get; set; } [JsonProperty("artist")] public string Artist { get; set; } + [JsonProperty("album_artist")] + public string AlbumArtist { get; set; } [JsonProperty("year")] public int? Year { get; set; } [JsonProperty("genre")] @@ -30,8 +32,12 @@ namespace Icarus.Models public string SongDirectory { get; set; } [JsonProperty("track")] public int Track { get; set; } = 0; + [JsonProperty("track_count")] + public int TrackCount { get; set; } = 0; [JsonProperty("disc")] public int Disc { get; set; } = 0; + [JsonProperty("disc_count")] + public int DiscCount { get; set; } = 0; [JsonIgnore] public int? AlbumID { get; set; } [JsonIgnore] diff --git a/Scripts/MySQL/create_database.sql b/Scripts/MySQL/create_database.sql index 68db808..de609e8 100644 --- a/Scripts/MySQL/create_database.sql +++ b/Scripts/MySQL/create_database.sql @@ -39,11 +39,14 @@ CREATE TABLE Song ( Title TEXT NOT NULL, Artist TEXT NOT NULL, Album TEXT NOT NULL, + AlbumArtist TEXT NOT NULL, Genre TEXT NOT NULL, Year INT NOT NULL, Duration INT NOT NULL, Track INT NOT NULL, + TrackCount INT NOT NULL, Disc INT NOT NULL, + DiscCount INT NOT NULL, SongDirectory TEXT NOT NULL, Filename TEXT NOT NULL, CoverArtID INT NOT NULL, diff --git a/Scripts/MySQL/delete_all.sql b/Scripts/MySQL/delete_all.sql index 6e336dd..8ce8313 100644 --- a/Scripts/MySQL/delete_all.sql +++ b/Scripts/MySQL/delete_all.sql @@ -2,5 +2,4 @@ delete from Song; delete from Album; delete from Artist; delete from Genre; -delete from Year; delete from CoverArt; diff --git a/Startup.cs b/Startup.cs index 689635c..44e5481 100644 --- a/Startup.cs +++ b/Startup.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; using NLog; using NLog.Web; using NLog.Web.AspNetCore; @@ -112,6 +113,9 @@ namespace Icarus services.AddDbContext(options => options.UseMySQL(connString)); services.AddDbContext(options => options.UseMySQL(connString)); services.AddDbContext(options => options.UseMySQL(connString)); + + services.AddControllers() + .AddNewtonsoftJson(); } // Called by the runtime. Use this method to configure the HTTP request pipeline.