diff --git a/.gitignore b/.gitignore index d47dc1c8..fd2143d8 100644 --- a/.gitignore +++ b/.gitignore @@ -515,3 +515,7 @@ compose.override.yml secrets.json *.secrets.json .aider* + +# Exclude build artifacts with unusual path separators +**/bin\\* +**/obj\\* diff --git a/docs/pages/playlists.md b/docs/pages/playlists.md index 7325b2df..ac2ada79 100644 --- a/docs/pages/playlists.md +++ b/docs/pages/playlists.md @@ -73,6 +73,61 @@ You can create playlists through: - **Melodee UI**: Navigate to Playlists and click "Create New Playlist" - **Music Clients**: Most Subsonic-compatible clients support playlist creation +- **M3U Import**: Upload existing M3U/M3U8 playlist files (see below) + +### Importing M3U/M3U8 Playlists + +Melodee can import your existing M3U or M3U8 playlist files, making it easy to migrate from other music players. + +**Supported formats:** +- `.m3u` - Standard M3U playlists +- `.m3u8` - UTF-8 encoded M3U playlists + +**How it works:** + +1. **Upload your playlist file** via the Melodee API or UI +2. **Automatic song matching** - Melodee tries to match each entry to songs in your library using: + - Exact file path matching (highest priority) + - Filename with artist/album folder hints + - Song metadata matching (title + artist + album) +3. **Instant playability** - Matched songs are immediately available in your new playlist +4. **Background reconciliation** - Missing songs are tracked and automatically added as you add music to your library + +**Import via API:** +```bash +curl -X POST https://your-melodee-server/api/v1/playlists/import \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -F "file=@my-playlist.m3u8" +``` + +**Response includes:** +- Playlist ID and name +- Total entries found +- Successfully matched songs +- Missing songs (tracked for future reconciliation) + +**Example response:** +```json +{ + "playlistId": "abc123...", + "playlistName": "My Favorite Mix", + "totalEntries": 25, + "matchedEntries": 20, + "missingEntries": 5 +} +``` + +**Background reconciliation:** + +Missing playlist items are automatically resolved when: +- New music is added to your library +- The reconciliation job runs periodically (hourly by default) + +The reconciliation process: +- Re-attempts matching for missing items +- Adds newly found songs to the playlist +- Maintains the original sort order +- Runs idempotently (no duplicates) ### Managing Playlists via Clients @@ -170,6 +225,13 @@ POST /api/v1/Songs/starred/{songId}/{isStarred} # Set rating POST /api/v1/Songs/setrating/{songId}/{rating} + +# Import M3U/M3U8 playlist +POST /api/v1/playlists/import +Content-Type: multipart/form-data +Body: file= + +# Response includes match statistics and playlist ID ``` ## Best Practices diff --git a/src/Melodee.Blazor/Components/Pages/Data/M3UPlaylistImportDialog.razor b/src/Melodee.Blazor/Components/Pages/Data/M3UPlaylistImportDialog.razor new file mode 100644 index 00000000..291eb1ef --- /dev/null +++ b/src/Melodee.Blazor/Components/Pages/Data/M3UPlaylistImportDialog.razor @@ -0,0 +1,231 @@ +@inherits MelodeeComponentBase +@using Melodee.Blazor.Controllers.Melodee.Models +@using Melodee.Common.Models +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.Extensions.Logging + +@inject PlaylistService PlaylistService +@inject NotificationService NotificationService +@inject DialogService DialogService +@inject IJSRuntime JS +@inject ILogger Logger + + + + @L("M3UImportDialog.Title") + + @L("M3UImportDialog.Description") + + + @L("M3UImportDialog.SupportedFormats"): +
    +
  • .m3u
  • +
  • .m3u8
  • +
+
+
+ + + + + + + @if (_isUploading) + { + + @L("M3UImportDialog.Uploading") + } + + @if (_validationErrors.Any()) + { + + @L("M3UImportDialog.ValidationErrors") +
    + @foreach (var error in _validationErrors) + { +
  • @error
  • + } +
+
+ } + + @if (_importResult != null) + { + + @L("M3UImportDialog.ImportSuccess") + + @L("Common.Name"): @_importResult.PlaylistName
+ @L("M3UImportDialog.TotalEntries"): @_importResult.TotalEntries
+ @L("M3UImportDialog.MatchedSongs"): @_importResult.MatchedEntries (@GetMatchPercentage()%)
+ @L("M3UImportDialog.MissingSongs"): @_importResult.MissingEntries
+
+ @if (_importResult.MissingEntries > 0) + { + + @L("M3UImportDialog.MissingItemsNote") + + } +
+ } + + + + + +
+ +@code { + private string _fileName = string.Empty; + private string _fileSize = string.Empty; + private IBrowserFile? _selectedFile; + private bool _isUploading; + private readonly List _validationErrors = new(); + private PlaylistImportResponse? _importResult; + + private const long MaxFileSize = 10 * 1024 * 1024; // 10MB + + private async Task TriggerFileInput() + { + await JS.InvokeVoidAsync("document.getElementById('m3uFileInput').click"); + } + + private async Task OnFileSelected(InputFileChangeEventArgs e) + { + _selectedFile = e.File; + _fileName = _selectedFile.Name; + _fileSize = FormatFileSize(_selectedFile.Size); + _validationErrors.Clear(); + _importResult = null; + + // Validate file + var extension = Path.GetExtension(_fileName).ToLowerInvariant(); + if (extension != ".m3u" && extension != ".m3u8") + { + _validationErrors.Add(L("M3UImportDialog.InvalidFileType")); + _selectedFile = null; + _fileName = string.Empty; + _fileSize = string.Empty; + StateHasChanged(); + return; + } + + if (_selectedFile.Size > MaxFileSize) + { + _validationErrors.Add(L("M3UImportDialog.FileTooLarge")); + _selectedFile = null; + _fileName = string.Empty; + _fileSize = string.Empty; + StateHasChanged(); + return; + } + + // Auto-upload if validation passes + await UploadFile(); + } + + private async Task UploadFile() + { + if (_selectedFile == null || _isUploading) + { + return; + } + + try + { + _isUploading = true; + _validationErrors.Clear(); + StateHasChanged(); + + using var stream = _selectedFile.OpenReadStream(MaxFileSize); + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + var result = await PlaylistService.ImportPlaylistAsync( + CurrentUser!.UserId(), + memoryStream, + _selectedFile.Name, + CancellationToken.None); + + if (!result.IsSuccess || result.Data == null) + { + var errorMessage = result.Errors?.FirstOrDefault()?.Message ?? L("M3UImportDialog.ImportFailed"); + _validationErrors.Add(errorMessage); + return; + } + + _importResult = new PlaylistImportResponse + { + PlaylistId = result.Data.PlaylistApiKey, + PlaylistName = result.Data.PlaylistName, + TotalEntries = result.Data.TotalEntries, + MatchedEntries = result.Data.MatchedEntries, + MissingEntries = result.Data.MissingEntries + }; + + NotificationService.Notify(new NotificationMessage + { + Severity = NotificationSeverity.Success, + Summary = L("M3UImportDialog.ImportSuccess"), + Detail = L("M3UImportDialog.ImportSummary", _importResult.MatchedEntries.ToString(), _importResult.TotalEntries.ToString()), + Duration = 5000 + }); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to import M3U playlist"); + _validationErrors.Add(L("M3UImportDialog.ImportFailed") + ": " + ex.Message); + NotificationService.Notify(new NotificationMessage + { + Severity = NotificationSeverity.Error, + Summary = L("M3UImportDialog.ImportFailed"), + Detail = ex.Message, + Duration = 8000 + }); + } + finally + { + _isUploading = false; + StateHasChanged(); + } + } + + private string FormatFileSize(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB" }; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + + private string GetMatchPercentage() + { + if (_importResult == null || _importResult.TotalEntries == 0) + { + return "0"; + } + return ((double)_importResult.MatchedEntries / _importResult.TotalEntries * 100).ToString("0.#"); + } + + private void CancelClick() + { + DialogService.Close(false); + } + + private void CloseClick() + { + DialogService.Close(true); + } +} diff --git a/src/Melodee.Blazor/Components/Pages/Data/Playlists.razor b/src/Melodee.Blazor/Components/Pages/Data/Playlists.razor index 27f18b39..59697da3 100644 --- a/src/Melodee.Blazor/Components/Pages/Data/Playlists.razor +++ b/src/Melodee.Blazor/Components/Pages/Data/Playlists.razor @@ -32,6 +32,10 @@ AlignItems="AlignItems.Center" JustifyContent="JustifyContent.End" Gap="0.5rem"> + ( + L("M3UImportDialog.Title"), + null, + new DialogOptions { Width = "700px", Height = "auto", Resizable = true, Draggable = true }); + + if (result is true) + { + await _grid.RefreshDataAsync(); + } + } + private async Task DeleteSelectedButtonClick() { var confirm = await DialogService.Confirm(L("Messages.ConfirmDelete"), L("Data.ConfirmDelete"), new ConfirmOptions { OkButtonText = L("Actions.Yes"), CancelButtonText = L("Actions.No") }); diff --git a/src/Melodee.Blazor/Controllers/Melodee/Models/PlaylistImportResponse.cs b/src/Melodee.Blazor/Controllers/Melodee/Models/PlaylistImportResponse.cs new file mode 100644 index 00000000..02bd280a --- /dev/null +++ b/src/Melodee.Blazor/Controllers/Melodee/Models/PlaylistImportResponse.cs @@ -0,0 +1,10 @@ +namespace Melodee.Blazor.Controllers.Melodee.Models; + +public sealed class PlaylistImportResponse +{ + public required Guid PlaylistId { get; init; } + public required string PlaylistName { get; init; } + public required int TotalEntries { get; init; } + public required int MatchedEntries { get; init; } + public required int MissingEntries { get; init; } +} diff --git a/src/Melodee.Blazor/Controllers/Melodee/PlaylistsController.cs b/src/Melodee.Blazor/Controllers/Melodee/PlaylistsController.cs index 4480a4b4..34a7e283 100644 --- a/src/Melodee.Blazor/Controllers/Melodee/PlaylistsController.cs +++ b/src/Melodee.Blazor/Controllers/Melodee/PlaylistsController.cs @@ -619,4 +619,68 @@ public async Task DeletePlaylistImage(Guid apiKey, CancellationTo return Ok(); } + + /// + /// Import an M3U/M3U8 playlist file. + /// + [HttpPost] + [Route("import")] + [ProducesResponseType(typeof(PlaylistImportResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)] + [RequestSizeLimit(10_000_000)] // 10MB limit for playlist files + public async Task ImportPlaylist(IFormFile file, CancellationToken cancellationToken = default) + { + if (!ApiRequest.IsAuthorized) + { + return ApiUnauthorized(); + } + + var user = await ResolveUserAsync(userService, cancellationToken).ConfigureAwait(false); + if (user == null) + { + return ApiUnauthorized(); + } + + if (user.IsLocked) + { + return ApiUserLocked(); + } + + if (file == null || file.Length == 0) + { + return ApiBadRequest("No file uploaded."); + } + + var fileName = file.FileName; + var isValidExtension = fileName.EndsWith(".m3u", StringComparison.OrdinalIgnoreCase) || + fileName.EndsWith(".m3u8", StringComparison.OrdinalIgnoreCase); + + if (!isValidExtension) + { + return ApiBadRequest("File must be an M3U or M3U8 playlist file."); + } + + await using var stream = file.OpenReadStream(); + var importResult = await playlistService.ImportPlaylistAsync( + user.Id, + stream, + fileName, + cancellationToken).ConfigureAwait(false); + + if (!importResult.IsSuccess || importResult.Data == null) + { + var errorMessage = importResult.Messages?.FirstOrDefault() ?? "Failed to import playlist."; + return ApiBadRequest(errorMessage); + } + + return Ok(new PlaylistImportResponse + { + PlaylistId = importResult.Data.PlaylistApiKey, + PlaylistName = importResult.Data.PlaylistName, + TotalEntries = importResult.Data.TotalEntries, + MatchedEntries = importResult.Data.MatchedEntries, + MissingEntries = importResult.Data.MissingEntries + }); + } } diff --git a/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx b/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx index 3e43dca2..73b2a599 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx @@ -5787,4 +5787,4 @@ [NEEDS TRANSLATION] Success - +[NEEDS TRANSLATION] Import M3U Playlist[NEEDS TRANSLATION] Import M3U/M3U8 Playlist[NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.[NEEDS TRANSLATION] Supported formats[NEEDS TRANSLATION] Standard M3U playlist[NEEDS TRANSLATION] UTF-8 encoded M3U playlist[NEEDS TRANSLATION] Select playlist file[NEEDS TRANSLATION] Uploading and processing playlist...[NEEDS TRANSLATION] Validation Errors[NEEDS TRANSLATION] Please select a valid M3U or M3U8 file[NEEDS TRANSLATION] File is too large. Maximum size is 10MB[NEEDS TRANSLATION] Import Successful[NEEDS TRANSLATION] Total entries[NEEDS TRANSLATION] Matched songs[NEEDS TRANSLATION] Missing songs[NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library.[NEEDS TRANSLATION] Import Failed[NEEDS TRANSLATION] Successfully imported {0} of {1} songs \ No newline at end of file diff --git a/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx b/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx index 5595e2b4..dfd5c254 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx b/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx index 7fce1879..2c4a9c71 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx @@ -5787,4 +5787,4 @@ [NEEDS TRANSLATION] Success - +[NEEDS TRANSLATION] Import M3U Playlist[NEEDS TRANSLATION] Import M3U/M3U8 Playlist[NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.[NEEDS TRANSLATION] Supported formats[NEEDS TRANSLATION] Standard M3U playlist[NEEDS TRANSLATION] UTF-8 encoded M3U playlist[NEEDS TRANSLATION] Select playlist file[NEEDS TRANSLATION] Uploading and processing playlist...[NEEDS TRANSLATION] Validation Errors[NEEDS TRANSLATION] Please select a valid M3U or M3U8 file[NEEDS TRANSLATION] File is too large. Maximum size is 10MB[NEEDS TRANSLATION] Import Successful[NEEDS TRANSLATION] Total entries[NEEDS TRANSLATION] Matched songs[NEEDS TRANSLATION] Missing songs[NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library.[NEEDS TRANSLATION] Import Failed[NEEDS TRANSLATION] Successfully imported {0} of {1} songs \ No newline at end of file diff --git a/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx b/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx index 49055908..a08c3a0c 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx @@ -5787,4 +5787,4 @@ [NEEDS TRANSLATION] Success - +[NEEDS TRANSLATION] Import M3U Playlist[NEEDS TRANSLATION] Import M3U/M3U8 Playlist[NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.[NEEDS TRANSLATION] Supported formats[NEEDS TRANSLATION] Standard M3U playlist[NEEDS TRANSLATION] UTF-8 encoded M3U playlist[NEEDS TRANSLATION] Select playlist file[NEEDS TRANSLATION] Uploading and processing playlist...[NEEDS TRANSLATION] Validation Errors[NEEDS TRANSLATION] Please select a valid M3U or M3U8 file[NEEDS TRANSLATION] File is too large. Maximum size is 10MB[NEEDS TRANSLATION] Import Successful[NEEDS TRANSLATION] Total entries[NEEDS TRANSLATION] Matched songs[NEEDS TRANSLATION] Missing songs[NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library.[NEEDS TRANSLATION] Import Failed[NEEDS TRANSLATION] Successfully imported {0} of {1} songs \ No newline at end of file diff --git a/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx b/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx index 226c8546..a8e81972 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx b/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx index 186ad0bc..f60f0034 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx @@ -5787,4 +5787,4 @@ [NEEDS TRANSLATION] Success - +[NEEDS TRANSLATION] Import M3U Playlist[NEEDS TRANSLATION] Import M3U/M3U8 Playlist[NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.[NEEDS TRANSLATION] Supported formats[NEEDS TRANSLATION] Standard M3U playlist[NEEDS TRANSLATION] UTF-8 encoded M3U playlist[NEEDS TRANSLATION] Select playlist file[NEEDS TRANSLATION] Uploading and processing playlist...[NEEDS TRANSLATION] Validation Errors[NEEDS TRANSLATION] Please select a valid M3U or M3U8 file[NEEDS TRANSLATION] File is too large. Maximum size is 10MB[NEEDS TRANSLATION] Import Successful[NEEDS TRANSLATION] Total entries[NEEDS TRANSLATION] Matched songs[NEEDS TRANSLATION] Missing songs[NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library.[NEEDS TRANSLATION] Import Failed[NEEDS TRANSLATION] Successfully imported {0} of {1} songs \ No newline at end of file diff --git a/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx b/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx index 86845da6..c638a668 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx b/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx index 850b4fd4..e8363897 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx @@ -5787,4 +5787,4 @@ [NEEDS TRANSLATION] Success - +[NEEDS TRANSLATION] Import M3U Playlist[NEEDS TRANSLATION] Import M3U/M3U8 Playlist[NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.[NEEDS TRANSLATION] Supported formats[NEEDS TRANSLATION] Standard M3U playlist[NEEDS TRANSLATION] UTF-8 encoded M3U playlist[NEEDS TRANSLATION] Select playlist file[NEEDS TRANSLATION] Uploading and processing playlist...[NEEDS TRANSLATION] Validation Errors[NEEDS TRANSLATION] Please select a valid M3U or M3U8 file[NEEDS TRANSLATION] File is too large. Maximum size is 10MB[NEEDS TRANSLATION] Import Successful[NEEDS TRANSLATION] Total entries[NEEDS TRANSLATION] Matched songs[NEEDS TRANSLATION] Missing songs[NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library.[NEEDS TRANSLATION] Import Failed[NEEDS TRANSLATION] Successfully imported {0} of {1} songs \ No newline at end of file diff --git a/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx b/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx index f878c19d..c07cead4 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx @@ -4692,7 +4692,7 @@ 構文ヘルプ - 例: artist:Beatles AND year:>=1970 + 例: artist:Beatles AND year:>=1970 @@ -5787,4 +5787,4 @@ [NEEDS TRANSLATION] Success - +[NEEDS TRANSLATION] Import M3U Playlist[NEEDS TRANSLATION] Import M3U/M3U8 Playlist[NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.[NEEDS TRANSLATION] Supported formats[NEEDS TRANSLATION] Standard M3U playlist[NEEDS TRANSLATION] UTF-8 encoded M3U playlist[NEEDS TRANSLATION] Select playlist file[NEEDS TRANSLATION] Uploading and processing playlist...[NEEDS TRANSLATION] Validation Errors[NEEDS TRANSLATION] Please select a valid M3U or M3U8 file[NEEDS TRANSLATION] File is too large. Maximum size is 10MB[NEEDS TRANSLATION] Import Successful[NEEDS TRANSLATION] Total entries[NEEDS TRANSLATION] Matched songs[NEEDS TRANSLATION] Missing songs[NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library.[NEEDS TRANSLATION] Import Failed[NEEDS TRANSLATION] Successfully imported {0} of {1} songs \ No newline at end of file diff --git a/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx b/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx index 6cbc4dbd..45c3a3f0 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx b/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx index 3939cebe..741cacff 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx b/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx index 3117e464..a305f968 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx b/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx index 9bd23626..008a949b 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx @@ -5787,4 +5787,4 @@ [NEEDS TRANSLATION] Success - +[NEEDS TRANSLATION] Import M3U Playlist[NEEDS TRANSLATION] Import M3U/M3U8 Playlist[NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.[NEEDS TRANSLATION] Supported formats[NEEDS TRANSLATION] Standard M3U playlist[NEEDS TRANSLATION] UTF-8 encoded M3U playlist[NEEDS TRANSLATION] Select playlist file[NEEDS TRANSLATION] Uploading and processing playlist...[NEEDS TRANSLATION] Validation Errors[NEEDS TRANSLATION] Please select a valid M3U or M3U8 file[NEEDS TRANSLATION] File is too large. Maximum size is 10MB[NEEDS TRANSLATION] Import Successful[NEEDS TRANSLATION] Total entries[NEEDS TRANSLATION] Matched songs[NEEDS TRANSLATION] Missing songs[NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library.[NEEDS TRANSLATION] Import Failed[NEEDS TRANSLATION] Successfully imported {0} of {1} songs \ No newline at end of file diff --git a/src/Melodee.Blazor/Resources/SharedResources.resx b/src/Melodee.Blazor/Resources/SharedResources.resx index 99126a3b..394c8450 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.resx @@ -5786,4 +5786,4 @@ Success - +Import M3U PlaylistImport M3U/M3U8 PlaylistUpload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.Supported formatsStandard M3U playlistUTF-8 encoded M3U playlistSelect playlist fileUploading and processing playlist...Validation ErrorsPlease select a valid M3U or M3U8 fileFile is too large. Maximum size is 10MBImport SuccessfulTotal entriesMatched songsMissing songsMissing songs will be automatically added to the playlist when they are added to your library.Import FailedSuccessfully imported {0} of {1} songs \ No newline at end of file diff --git a/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx b/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx index 9a0da058..1adab625 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx @@ -5787,4 +5787,4 @@ [NEEDS TRANSLATION] Success - +[NEEDS TRANSLATION] Import M3U Playlist[NEEDS TRANSLATION] Import M3U/M3U8 Playlist[NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.[NEEDS TRANSLATION] Supported formats[NEEDS TRANSLATION] Standard M3U playlist[NEEDS TRANSLATION] UTF-8 encoded M3U playlist[NEEDS TRANSLATION] Select playlist file[NEEDS TRANSLATION] Uploading and processing playlist...[NEEDS TRANSLATION] Validation Errors[NEEDS TRANSLATION] Please select a valid M3U or M3U8 file[NEEDS TRANSLATION] File is too large. Maximum size is 10MB[NEEDS TRANSLATION] Import Successful[NEEDS TRANSLATION] Total entries[NEEDS TRANSLATION] Matched songs[NEEDS TRANSLATION] Missing songs[NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library.[NEEDS TRANSLATION] Import Failed[NEEDS TRANSLATION] Successfully imported {0} of {1} songs \ No newline at end of file diff --git a/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx b/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx index e90ad3d5..7f6de758 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx b/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx index 26f77115..e0823712 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx b/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx index 98b88889..ef665ebb 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx b/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx index 021fda34..cab484a2 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx @@ -5785,4 +5785,59 @@ [NEEDS TRANSLATION] Success + + [NEEDS TRANSLATION] Import M3U Playlist + + + [NEEDS TRANSLATION] Import M3U/M3U8 Playlist + + + [NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist. + + + [NEEDS TRANSLATION] Supported formats + + + [NEEDS TRANSLATION] Standard M3U playlist + + + [NEEDS TRANSLATION] UTF-8 encoded M3U playlist + + + [NEEDS TRANSLATION] Select playlist file + + + [NEEDS TRANSLATION] Uploading and processing playlist... + + + [NEEDS TRANSLATION] Validation Errors + + + [NEEDS TRANSLATION] Please select a valid M3U or M3U8 file + + + [NEEDS TRANSLATION] File is too large. Maximum size is 10MB + + + [NEEDS TRANSLATION] Import Successful + + + [NEEDS TRANSLATION] Total entries + + + [NEEDS TRANSLATION] Matched songs + + + [NEEDS TRANSLATION] Missing songs + + + [NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library. + + + [NEEDS TRANSLATION] Import Failed + + + [NEEDS TRANSLATION] Successfully imported {0} of {1} songs + + diff --git a/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx b/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx index 20b9bcc6..2d1e877c 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx @@ -5787,4 +5787,4 @@ [NEEDS TRANSLATION] Success - +[NEEDS TRANSLATION] Import M3U Playlist[NEEDS TRANSLATION] Import M3U/M3U8 Playlist[NEEDS TRANSLATION] Upload your existing M3U or M3U8 playlist file. Melodee will automatically match songs in your library and create a playable playlist.[NEEDS TRANSLATION] Supported formats[NEEDS TRANSLATION] Standard M3U playlist[NEEDS TRANSLATION] UTF-8 encoded M3U playlist[NEEDS TRANSLATION] Select playlist file[NEEDS TRANSLATION] Uploading and processing playlist...[NEEDS TRANSLATION] Validation Errors[NEEDS TRANSLATION] Please select a valid M3U or M3U8 file[NEEDS TRANSLATION] File is too large. Maximum size is 10MB[NEEDS TRANSLATION] Import Successful[NEEDS TRANSLATION] Total entries[NEEDS TRANSLATION] Matched songs[NEEDS TRANSLATION] Missing songs[NEEDS TRANSLATION] Missing songs will be automatically added to the playlist when they are added to your library.[NEEDS TRANSLATION] Import Failed[NEEDS TRANSLATION] Successfully imported {0} of {1} songs \ No newline at end of file diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.Build.Locator.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.Build.Locator.dll" new file mode 100755 index 00000000..13b1021e Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.Build.Locator.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe" new file mode 100755 index 00000000..00dd99f7 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe.config" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe.config" new file mode 100755 index 00000000..f52998b2 --- /dev/null +++ "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe.config" @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.IO.Redist.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.IO.Redist.dll" new file mode 100755 index 00000000..88e63d82 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Microsoft.IO.Redist.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Newtonsoft.Json.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Newtonsoft.Json.dll" new file mode 100755 index 00000000..1d035d63 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/Newtonsoft.Json.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Buffers.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Buffers.dll" new file mode 100755 index 00000000..f2d83c51 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Buffers.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Collections.Immutable.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Collections.Immutable.dll" new file mode 100755 index 00000000..7594b2e1 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Collections.Immutable.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.CommandLine.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.CommandLine.dll" new file mode 100755 index 00000000..d0bbad5d Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.CommandLine.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Memory.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Memory.dll" new file mode 100755 index 00000000..46171997 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Memory.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Numerics.Vectors.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Numerics.Vectors.dll" new file mode 100755 index 00000000..08659724 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Numerics.Vectors.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Runtime.CompilerServices.Unsafe.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Runtime.CompilerServices.Unsafe.dll" new file mode 100755 index 00000000..c5ba4e40 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Runtime.CompilerServices.Unsafe.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Threading.Tasks.Extensions.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Threading.Tasks.Extensions.dll" new file mode 100755 index 00000000..eeec9285 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/System.Threading.Tasks.Extensions.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/cs/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/cs/System.CommandLine.resources.dll" new file mode 100755 index 00000000..0be3757c Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/cs/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/de/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/de/System.CommandLine.resources.dll" new file mode 100755 index 00000000..bfed293e Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/de/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/es/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/es/System.CommandLine.resources.dll" new file mode 100755 index 00000000..5e1c416f Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/es/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/fr/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/fr/System.CommandLine.resources.dll" new file mode 100755 index 00000000..2916bdf2 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/fr/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/it/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/it/System.CommandLine.resources.dll" new file mode 100755 index 00000000..1a55c94f Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/it/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/ja/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/ja/System.CommandLine.resources.dll" new file mode 100755 index 00000000..c1be1539 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/ja/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/ko/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/ko/System.CommandLine.resources.dll" new file mode 100755 index 00000000..bfcbbc61 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/ko/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/pl/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/pl/System.CommandLine.resources.dll" new file mode 100755 index 00000000..b9efaec6 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/pl/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/pt-BR/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/pt-BR/System.CommandLine.resources.dll" new file mode 100755 index 00000000..69612cbc Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/pt-BR/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/ru/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/ru/System.CommandLine.resources.dll" new file mode 100755 index 00000000..042aaf8e Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/ru/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/tr/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/tr/System.CommandLine.resources.dll" new file mode 100755 index 00000000..629b98b4 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/tr/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/zh-Hans/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/zh-Hans/System.CommandLine.resources.dll" new file mode 100755 index 00000000..ff8dacbf Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/zh-Hans/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/zh-Hant/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/zh-Hant/System.CommandLine.resources.dll" new file mode 100755 index 00000000..9b9870a0 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-net472/zh-Hant/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.Build.Locator.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.Build.Locator.dll" new file mode 100755 index 00000000..cafcf213 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.Build.Locator.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.deps.json" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.deps.json" new file mode 100755 index 00000000..ed7fe7a5 --- /dev/null +++ "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.deps.json" @@ -0,0 +1,260 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/4.14.0-3.25262.10": { + "dependencies": { + "Microsoft.Build.Locator": "1.6.10", + "Microsoft.CodeAnalysis.NetAnalyzers": "8.0.0-preview.23468.1", + "Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers": "3.3.4-beta1.22504.1", + "Microsoft.DotNet.XliffTasks": "9.0.0-beta.25255.5", + "Microsoft.VisualStudio.Threading.Analyzers": "17.13.2", + "Newtonsoft.Json": "13.0.3", + "Roslyn.Diagnostics.Analyzers": "3.11.0-beta1.24081.1", + "System.Collections.Immutable": "9.0.0", + "System.CommandLine": "2.0.0-beta4.24528.1" + }, + "runtime": { + "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll": {} + }, + "resources": { + "cs/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "cs" + }, + "de/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "de" + }, + "es/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "es" + }, + "fr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "fr" + }, + "it/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "it" + }, + "ja/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "ja" + }, + "ko/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "ko" + }, + "pl/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "pl" + }, + "pt-BR/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "pt-BR" + }, + "ru/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "ru" + }, + "tr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "tr" + }, + "zh-Hans/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "zh-Hans" + }, + "zh-Hant/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "Microsoft.Build.Locator/1.6.10": { + "runtime": { + "lib/net6.0/Microsoft.Build.Locator.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.6.10.57384" + } + } + }, + "Microsoft.CodeAnalysis.BannedApiAnalyzers/3.11.0-beta1.24081.1": {}, + "Microsoft.CodeAnalysis.NetAnalyzers/8.0.0-preview.23468.1": {}, + "Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers/3.3.4-beta1.22504.1": {}, + "Microsoft.CodeAnalysis.PublicApiAnalyzers/3.11.0-beta1.24081.1": {}, + "Microsoft.DotNet.XliffTasks/9.0.0-beta.25255.5": {}, + "Microsoft.VisualStudio.Threading.Analyzers/17.13.2": {}, + "Newtonsoft.Json/13.0.3": { + "runtime": { + "lib/net6.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.3.27908" + } + } + }, + "Roslyn.Diagnostics.Analyzers/3.11.0-beta1.24081.1": { + "dependencies": { + "Microsoft.CodeAnalysis.BannedApiAnalyzers": "3.11.0-beta1.24081.1", + "Microsoft.CodeAnalysis.PublicApiAnalyzers": "3.11.0-beta1.24081.1" + } + }, + "System.Collections.Immutable/9.0.0": { + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + }, + "runtime": { + "lib/netstandard2.0/System.Collections.Immutable.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "System.CommandLine/2.0.0-beta4.24528.1": { + "dependencies": { + "System.Memory": "4.5.5" + }, + "runtime": { + "lib/netstandard2.0/System.CommandLine.dll": { + "assemblyVersion": "2.0.0.0", + "fileVersion": "2.0.24.52801" + } + }, + "resources": { + "lib/netstandard2.0/cs/System.CommandLine.resources.dll": { + "locale": "cs" + }, + "lib/netstandard2.0/de/System.CommandLine.resources.dll": { + "locale": "de" + }, + "lib/netstandard2.0/es/System.CommandLine.resources.dll": { + "locale": "es" + }, + "lib/netstandard2.0/fr/System.CommandLine.resources.dll": { + "locale": "fr" + }, + "lib/netstandard2.0/it/System.CommandLine.resources.dll": { + "locale": "it" + }, + "lib/netstandard2.0/ja/System.CommandLine.resources.dll": { + "locale": "ja" + }, + "lib/netstandard2.0/ko/System.CommandLine.resources.dll": { + "locale": "ko" + }, + "lib/netstandard2.0/pl/System.CommandLine.resources.dll": { + "locale": "pl" + }, + "lib/netstandard2.0/pt-BR/System.CommandLine.resources.dll": { + "locale": "pt-BR" + }, + "lib/netstandard2.0/ru/System.CommandLine.resources.dll": { + "locale": "ru" + }, + "lib/netstandard2.0/tr/System.CommandLine.resources.dll": { + "locale": "tr" + }, + "lib/netstandard2.0/zh-Hans/System.CommandLine.resources.dll": { + "locale": "zh-Hans" + }, + "lib/netstandard2.0/zh-Hant/System.CommandLine.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "System.Memory/4.5.5": {}, + "System.Runtime.CompilerServices.Unsafe/6.0.0": {} + } + }, + "libraries": { + "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/4.14.0-3.25262.10": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.Build.Locator/1.6.10": { + "type": "package", + "serviceable": true, + "sha512": "sha512-DJhCkTGqy1LMJzEmG/2qxRTMHwdPc3WdVoGQI5o5mKHVo4dsHrCMLIyruwU/NSvPNSdvONlaf7jdFXnAMuxAuA==", + "path": "microsoft.build.locator/1.6.10", + "hashPath": "microsoft.build.locator.1.6.10.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.BannedApiAnalyzers/3.11.0-beta1.24081.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-DH6L3rsbjppLrHM2l2/NKbnMaYd0NFHx2pjZaFdrVcRkONrV3i9FHv6Id8Dp6/TmjhXQsJVJJFbhhjkpuP1xxg==", + "path": "microsoft.codeanalysis.bannedapianalyzers/3.11.0-beta1.24081.1", + "hashPath": "microsoft.codeanalysis.bannedapianalyzers.3.11.0-beta1.24081.1.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.NetAnalyzers/8.0.0-preview.23468.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ZhIvyxmUCqb8OiU/VQfxfuAmIB4lQsjqhMVYKeoyxzSI+d7uR5Pzx3ZKoaIhPizQ15wa4lnyD6wg3TnSJ6P4LA==", + "path": "microsoft.codeanalysis.netanalyzers/8.0.0-preview.23468.1", + "hashPath": "microsoft.codeanalysis.netanalyzers.8.0.0-preview.23468.1.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers/3.3.4-beta1.22504.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-2XRlqPAzVke7Sb80+UqaC7o57OwfK+tIr+aIOxrx41RWDMeR2SBUW7kL4sd6hfLFfBNsLo3W5PT+UwfvwPaOzA==", + "path": "microsoft.codeanalysis.performancesensitiveanalyzers/3.3.4-beta1.22504.1", + "hashPath": "microsoft.codeanalysis.performancesensitiveanalyzers.3.3.4-beta1.22504.1.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers/3.11.0-beta1.24081.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-3bYGBihvoNO0rhCOG1U9O50/4Q8suZ+glHqQLIAcKvnodSnSW+dYWYzTNb1UbS8pUS8nAUfxSFMwuMup/G5DtQ==", + "path": "microsoft.codeanalysis.publicapianalyzers/3.11.0-beta1.24081.1", + "hashPath": "microsoft.codeanalysis.publicapianalyzers.3.11.0-beta1.24081.1.nupkg.sha512" + }, + "Microsoft.DotNet.XliffTasks/9.0.0-beta.25255.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-bb0fZB5ViPscdfYeWlmtyXJMzNkgcpkV5RWmXktfV9lwIUZgNZmFotUXrdcTyZzrN7v1tQK/Y6BGnbkP9gEsXg==", + "path": "microsoft.dotnet.xlifftasks/9.0.0-beta.25255.5", + "hashPath": "microsoft.dotnet.xlifftasks.9.0.0-beta.25255.5.nupkg.sha512" + }, + "Microsoft.VisualStudio.Threading.Analyzers/17.13.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Qcd8IlaTXZVq3wolBnzby1P7kWihdWaExtD8riumiKuG1sHa8EgjV/o70TMjTaeUMhomBbhfdC9OPwAHoZfnjQ==", + "path": "microsoft.visualstudio.threading.analyzers/17.13.2", + "hashPath": "microsoft.visualstudio.threading.analyzers.17.13.2.nupkg.sha512" + }, + "Newtonsoft.Json/13.0.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==", + "path": "newtonsoft.json/13.0.3", + "hashPath": "newtonsoft.json.13.0.3.nupkg.sha512" + }, + "Roslyn.Diagnostics.Analyzers/3.11.0-beta1.24081.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-reHqZCDKifA+DURcL8jUfYkMGL4FpgNt5LI0uWTS6IpM8kKVbu/kO8byZsqfhBu4wUzT3MBDcoMfzhZPdENIpg==", + "path": "roslyn.diagnostics.analyzers/3.11.0-beta1.24081.1", + "hashPath": "roslyn.diagnostics.analyzers.3.11.0-beta1.24081.1.nupkg.sha512" + }, + "System.Collections.Immutable/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==", + "path": "system.collections.immutable/9.0.0", + "hashPath": "system.collections.immutable.9.0.0.nupkg.sha512" + }, + "System.CommandLine/2.0.0-beta4.24528.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Xt8tsSU8yd0ZpbT9gl5DAwkMYWLo8PV1fq2R/belrUbHVVOIKqhLfbWksbdknUDpmzMHZenBtD6AGAp9uJTa2w==", + "path": "system.commandline/2.0.0-beta4.24528.1", + "hashPath": "system.commandline.2.0.0-beta4.24528.1.nupkg.sha512" + }, + "System.Memory/4.5.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "path": "system.memory/4.5.5", + "hashPath": "system.memory.4.5.5.nupkg.sha512" + }, + "System.Runtime.CompilerServices.Unsafe/6.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", + "path": "system.runtime.compilerservices.unsafe/6.0.0", + "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512" + } + } +} \ No newline at end of file diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll" new file mode 100755 index 00000000..993b54f2 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll.config" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll.config" new file mode 100755 index 00000000..27bdea78 --- /dev/null +++ "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll.config" @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.runtimeconfig.json" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.runtimeconfig.json" new file mode 100755 index 00000000..3a5998aa --- /dev/null +++ "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.runtimeconfig.json" @@ -0,0 +1,13 @@ +{ + "runtimeOptions": { + "tfm": "net6.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "6.0.0" + }, + "rollForward": "Major", + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false + } + } +} \ No newline at end of file diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Newtonsoft.Json.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Newtonsoft.Json.dll" new file mode 100755 index 00000000..87bf9aab Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/Newtonsoft.Json.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/System.Collections.Immutable.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/System.Collections.Immutable.dll" new file mode 100755 index 00000000..b1821271 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/System.Collections.Immutable.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/System.CommandLine.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/System.CommandLine.dll" new file mode 100755 index 00000000..d0bbad5d Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/System.CommandLine.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/cs/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/cs/System.CommandLine.resources.dll" new file mode 100755 index 00000000..0be3757c Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/cs/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/de/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/de/System.CommandLine.resources.dll" new file mode 100755 index 00000000..bfed293e Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/de/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/es/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/es/System.CommandLine.resources.dll" new file mode 100755 index 00000000..5e1c416f Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/es/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/fr/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/fr/System.CommandLine.resources.dll" new file mode 100755 index 00000000..2916bdf2 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/fr/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/it/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/it/System.CommandLine.resources.dll" new file mode 100755 index 00000000..1a55c94f Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/it/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/ja/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/ja/System.CommandLine.resources.dll" new file mode 100755 index 00000000..c1be1539 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/ja/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/ko/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/ko/System.CommandLine.resources.dll" new file mode 100755 index 00000000..bfcbbc61 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/ko/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/pl/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/pl/System.CommandLine.resources.dll" new file mode 100755 index 00000000..b9efaec6 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/pl/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/pt-BR/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/pt-BR/System.CommandLine.resources.dll" new file mode 100755 index 00000000..69612cbc Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/pt-BR/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/ru/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/ru/System.CommandLine.resources.dll" new file mode 100755 index 00000000..042aaf8e Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/ru/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/tr/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/tr/System.CommandLine.resources.dll" new file mode 100755 index 00000000..629b98b4 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/tr/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/zh-Hans/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/zh-Hans/System.CommandLine.resources.dll" new file mode 100755 index 00000000..ff8dacbf Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/zh-Hans/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/zh-Hant/System.CommandLine.resources.dll" "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/zh-Hant/System.CommandLine.resources.dll" new file mode 100755 index 00000000..9b9870a0 Binary files /dev/null and "b/src/Melodee.Blazor/bin\\Debug/net10.0/BuildHost-netcore/zh-Hant/System.CommandLine.resources.dll" differ diff --git a/src/Melodee.Common/Data/MelodeeDbContext.cs b/src/Melodee.Common/Data/MelodeeDbContext.cs index 67e4dd12..5d8f498a 100644 --- a/src/Melodee.Common/Data/MelodeeDbContext.cs +++ b/src/Melodee.Common/Data/MelodeeDbContext.cs @@ -105,6 +105,10 @@ public class MelodeeDbContext(DbContextOptions options) : DbCo public DbSet PartyAuditEvents { get; set; } + public DbSet PlaylistUploadedFiles { get; set; } + + public DbSet PlaylistUploadedFileItems { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { // Use a fixed timestamp for seed data to prevent migration churn diff --git a/src/Melodee.Common/Data/Models/Playlist.cs b/src/Melodee.Common/Data/Models/Playlist.cs index 0bc42000..0029b823 100644 --- a/src/Melodee.Common/Data/Models/Playlist.cs +++ b/src/Melodee.Common/Data/Models/Playlist.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Melodee.Common.Data.Constants; using Melodee.Common.Data.Validators; +using Melodee.Common.Enums; using Microsoft.EntityFrameworkCore; namespace Melodee.Common.Data.Models; @@ -28,6 +29,18 @@ public class Playlist : DataModelBase public User User { get; set; } = null!; + /// + /// Source type of the playlist (Manual, M3UImport, Dynamic) + /// + public PlaylistSourceType SourceType { get; set; } = PlaylistSourceType.Manual; + + /// + /// Reference to the uploaded file if this playlist was created from an import + /// + public int? PlaylistUploadedFileId { get; set; } + + public PlaylistUploadedFile? PlaylistUploadedFile { get; set; } + public bool IsPublic { get; set; } public short? SongCount { get; set; } diff --git a/src/Melodee.Common/Data/Models/PlaylistUploadedFile.cs b/src/Melodee.Common/Data/Models/PlaylistUploadedFile.cs new file mode 100644 index 00000000..e1f68f2b --- /dev/null +++ b/src/Melodee.Common/Data/Models/PlaylistUploadedFile.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using Melodee.Common.Data.Constants; +using Melodee.Common.Data.Validators; +using Microsoft.EntityFrameworkCore; + +namespace Melodee.Common.Data.Models; + +[Serializable] +[Index(nameof(UserId))] +public class PlaylistUploadedFile : DataModelBase +{ + [RequiredGreaterThanZero] + public int UserId { get; set; } + + public User User { get; set; } = null!; + + [Required] + [MaxLength(MaxLengthDefinitions.MaxGeneralInputLength)] + public required string OriginalFileName { get; set; } + + [Required] + [MaxLength(MaxLengthDefinitions.MaxGeneralInputLength)] + public required string ContentType { get; set; } + + [RequiredGreaterThanZero] + public required long Length { get; set; } + + /// + /// Original uploaded file data for traceability and re-processing + /// + public byte[]? FileData { get; set; } + + public ICollection Items { get; set; } = new List(); +} diff --git a/src/Melodee.Common/Data/Models/PlaylistUploadedFileItem.cs b/src/Melodee.Common/Data/Models/PlaylistUploadedFileItem.cs new file mode 100644 index 00000000..9ddf3f3b --- /dev/null +++ b/src/Melodee.Common/Data/Models/PlaylistUploadedFileItem.cs @@ -0,0 +1,58 @@ +using System.ComponentModel.DataAnnotations; +using Melodee.Common.Data.Constants; +using Melodee.Common.Data.Validators; +using Melodee.Common.Enums; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Melodee.Common.Data.Models; + +[Serializable] +[Index(nameof(PlaylistUploadedFileId), nameof(SortOrder))] +public class PlaylistUploadedFileItem +{ + public int Id { get; set; } + + [RequiredGreaterThanZero] + public int PlaylistUploadedFileId { get; set; } + + public PlaylistUploadedFile PlaylistUploadedFile { get; set; } = null!; + + /// + /// The resolved song ID, null if not yet resolved + /// + public int? SongId { get; set; } + + public Song? Song { get; set; } + + [Required] + public int SortOrder { get; set; } + + [Required] + public PlaylistItemStatus Status { get; set; } = PlaylistItemStatus.Missing; + + /// + /// Original raw line from the playlist file + /// + [Required] + [MaxLength(MaxLengthDefinitions.MaxGeneralLongLength)] + public required string RawReference { get; set; } + + /// + /// Normalized reference (decoded, path separators normalized) + /// + [Required] + [MaxLength(MaxLengthDefinitions.MaxGeneralLongLength)] + public required string NormalizedReference { get; set; } + + /// + /// JSON with hints for matching: filename, artistFolder, albumFolder, etc. + /// + [MaxLength(MaxLengthDefinitions.MaxInputLength)] + public string? HintsJson { get; set; } + + /// + /// Last time we attempted to resolve this item + /// + public Instant? LastAttemptUtc { get; set; } +} diff --git a/src/Melodee.Common/Enums/PlaylistItemStatus.cs b/src/Melodee.Common/Enums/PlaylistItemStatus.cs new file mode 100644 index 00000000..20a3e0f5 --- /dev/null +++ b/src/Melodee.Common/Enums/PlaylistItemStatus.cs @@ -0,0 +1,7 @@ +namespace Melodee.Common.Enums; + +public enum PlaylistItemStatus : short +{ + Resolved = 0, + Missing = 1 +} diff --git a/src/Melodee.Common/Enums/PlaylistSourceType.cs b/src/Melodee.Common/Enums/PlaylistSourceType.cs new file mode 100644 index 00000000..7b5d484b --- /dev/null +++ b/src/Melodee.Common/Enums/PlaylistSourceType.cs @@ -0,0 +1,8 @@ +namespace Melodee.Common.Enums; + +public enum PlaylistSourceType : short +{ + Manual = 0, + M3UImport = 1, + Dynamic = 2 +} diff --git a/src/Melodee.Common/Jobs/PlaylistReconciliationJob.cs b/src/Melodee.Common/Jobs/PlaylistReconciliationJob.cs new file mode 100644 index 00000000..1bbf749a --- /dev/null +++ b/src/Melodee.Common/Jobs/PlaylistReconciliationJob.cs @@ -0,0 +1,143 @@ +using Melodee.Common.Configuration; +using Melodee.Common.Data; +using Melodee.Common.Enums; +using Melodee.Common.Services; +using Microsoft.EntityFrameworkCore; +using NodaTime; +using Quartz; +using Serilog; + +namespace Melodee.Common.Jobs; + +/// +/// Re-attempts matching for missing playlist items when new music is added to the library. +/// Runs periodically or can be triggered after library scans. +/// +[DisallowConcurrentExecution] +public sealed class PlaylistReconciliationJob( + ILogger logger, + IMelodeeConfigurationFactory configurationFactory, + IDbContextFactory contextFactory, + LibraryService libraryService, + Services.Caching.ICacheManager cacheManager) : JobBase(logger, configurationFactory) +{ + public override bool DoCreateJobHistory => true; + + public override async Task Execute(IJobExecutionContext context) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(context.CancellationToken).ConfigureAwait(false); + + // Find all missing playlist items that haven't been attempted recently + var retryThreshold = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); + + var missingItems = await dbContext.PlaylistUploadedFileItems + .Where(x => x.Status == PlaylistItemStatus.Missing) + .Where(x => x.LastAttemptUtc == null || x.LastAttemptUtc < retryThreshold) + .Include(x => x.PlaylistUploadedFile) + .ThenInclude(f => f.Items) + .OrderBy(x => x.LastAttemptUtc) + .Take(500) // Process in batches to avoid overwhelming the database + .ToListAsync(context.CancellationToken) + .ConfigureAwait(false); + + if (missingItems.Count == 0) + { + Logger.Debug("[{JobName}] No missing playlist items to reconcile.", nameof(PlaylistReconciliationJob)); + return; + } + + Logger.Information("[{JobName}] Attempting to reconcile {ItemCount} missing playlist items.", + nameof(PlaylistReconciliationJob), missingItems.Count); + + // Get library path for matching + var librariesResult = await libraryService.GetStorageLibrariesAsync(context.CancellationToken).ConfigureAwait(false); + var libraryPath = librariesResult.Data?.FirstOrDefault()?.Path; + + var songMatcher = new SongMatchingService(Logger, cacheManager, contextFactory); + + var resolvedCount = 0; + var now = SystemClock.Instance.GetCurrentInstant(); + + foreach (var item in missingItems) + { + try + { + // Reconstruct M3UEntry from stored data + var entry = new Services.Parsing.M3UEntry + { + RawReference = item.RawReference, + NormalizedReference = item.NormalizedReference, + SortOrder = item.SortOrder, + // Hints are stored in JSON, but for now we'll extract them from normalized reference + FileName = item.NormalizedReference.Split('/').LastOrDefault(), + ArtistFolder = null, + AlbumFolder = null + }; + + var matchResult = await songMatcher.MatchEntryAsync(entry, libraryPath, context.CancellationToken) + .ConfigureAwait(false); + + // Update the item + item.LastAttemptUtc = now; + + if (matchResult.Song != null) + { + item.SongId = matchResult.Song.Id; + item.Status = PlaylistItemStatus.Resolved; + resolvedCount++; + + // Find the associated playlist and add the song + var playlist = await dbContext.Playlists + .Include(p => p.Songs) + .FirstOrDefaultAsync(p => p.PlaylistUploadedFileId == item.PlaylistUploadedFileId, + context.CancellationToken) + .ConfigureAwait(false); + + if (playlist != null) + { + // Check if this song is already in the playlist to maintain idempotency + var existingPlaylistSong = playlist.Songs + .FirstOrDefault(ps => ps.SongId == matchResult.Song.Id); + + if (existingPlaylistSong == null) + { + // Add song to playlist maintaining sort order + var maxOrder = playlist.Songs.Any() ? playlist.Songs.Max(ps => ps.PlaylistOrder) : -1; + + var playlistSong = new Data.Models.PlaylistSong + { + PlaylistId = playlist.Id, + SongId = matchResult.Song.Id, + SongApiKey = matchResult.Song.ApiKey, + PlaylistOrder = maxOrder + 1 + }; + + playlist.Songs.Add(playlistSong); + playlist.SongCount = (short)playlist.Songs.Count; + + // Efficiently calculate duration by loading song from matchResult instead of querying + var currentDuration = playlist.Songs + .Where(ps => ps.SongId != matchResult.Song.Id) + .Sum(ps => dbContext.Songs.First(s => s.Id == ps.SongId).Duration); + playlist.Duration = currentDuration + matchResult.Song.Duration; + + Logger.Debug("[{JobName}] Resolved missing item for playlist [{PlaylistId}]: {Reference}", + nameof(PlaylistReconciliationJob), playlist.Id, item.NormalizedReference); + } + } + } + } + catch (Exception ex) + { + Logger.Warning(ex, "[{JobName}] Error reconciling item: {Reference}", + nameof(PlaylistReconciliationJob), item.NormalizedReference); + item.LastAttemptUtc = now; + } + } + + await dbContext.SaveChangesAsync(context.CancellationToken).ConfigureAwait(false); + + Logger.Information("[{JobName}] Reconciliation complete. Resolved {ResolvedCount}/{TotalCount} items.", + nameof(PlaylistReconciliationJob), resolvedCount, missingItems.Count); + } +} diff --git a/src/Melodee.Common/Migrations/20260117043252_AddPlaylistImportModels.Designer.cs b/src/Melodee.Common/Migrations/20260117043252_AddPlaylistImportModels.Designer.cs new file mode 100644 index 00000000..2b56743f --- /dev/null +++ b/src/Melodee.Common/Migrations/20260117043252_AddPlaylistImportModels.Designer.cs @@ -0,0 +1,6255 @@ +// +using System; +using Melodee.Common.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Melodee.Common.Migrations +{ + [DbContext(typeof(MelodeeDbContext))] + [Migration("20260117043252_AddPlaylistImportModels")] + partial class AddPlaylistImportModels + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Melodee.Common.Data.Models.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumStatus") + .HasColumnType("smallint"); + + b.Property("AlbumType") + .HasColumnType("smallint"); + + b.Property("AlternateNames") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("AmgId") + .HasColumnType("text"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("ArtistId") + .HasColumnType("integer"); + + b.Property("CalculatedRating") + .HasColumnType("numeric"); + + b.Property("Comment") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeezerId") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("Directory") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DiscogsId") + .HasColumnType("text"); + + b.Property("Duration") + .HasColumnType("double precision"); + + b.PrimitiveCollection("Genres") + .HasMaxLength(2000) + .HasColumnType("text[]"); + + b.Property("ImageCount") + .HasColumnType("integer"); + + b.Property("IsCompilation") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("ItunesId") + .HasColumnType("text"); + + b.Property("LastFmId") + .HasColumnType("text"); + + b.Property("LastMetaDataUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPlayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MetaDataStatus") + .HasColumnType("integer"); + + b.PrimitiveCollection("Moods") + .HasMaxLength(2000) + .HasColumnType("text[]"); + + b.Property("MusicBrainzId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("OriginalReleaseDate") + .HasColumnType("date"); + + b.Property("PlayedCount") + .HasColumnType("integer"); + + b.Property("ReleaseDate") + .HasColumnType("date"); + + b.Property("ReplayGain") + .HasColumnType("double precision"); + + b.Property("ReplayPeak") + .HasColumnType("double precision"); + + b.Property("SongCount") + .HasColumnType("smallint"); + + b.Property("SortName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("SpotifyId") + .HasColumnType("text"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("WikiDataId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("MusicBrainzId") + .IsUnique(); + + b.HasIndex("SpotifyId") + .IsUnique(); + + b.HasIndex("ArtistId", "Name") + .IsUnique(); + + b.HasIndex("ArtistId", "NameNormalized") + .IsUnique(); + + b.HasIndex("ArtistId", "SortName") + .IsUnique(); + + b.ToTable("Albums"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Artist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumCount") + .HasColumnType("integer"); + + b.Property("AlternateNames") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("AmgId") + .HasColumnType("text"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("Biography") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("CalculatedRating") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeezerId") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("Directory") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DiscogsId") + .HasColumnType("text"); + + b.Property("ImageCount") + .HasColumnType("integer"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("ItunesId") + .HasColumnType("text"); + + b.Property("LastFmId") + .HasColumnType("text"); + + b.Property("LastMetaDataUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPlayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LibraryId") + .HasColumnType("integer"); + + b.Property("MetaDataStatus") + .HasColumnType("integer"); + + b.Property("MusicBrainzId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PlayedCount") + .HasColumnType("integer"); + + b.Property("RealName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Roles") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("SongCount") + .HasColumnType("integer"); + + b.Property("SortName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("SpotifyId") + .HasColumnType("text"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("WikiDataId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("LibraryId"); + + b.HasIndex("MusicBrainzId") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("NameNormalized"); + + b.HasIndex("SortName"); + + b.HasIndex("SpotifyId") + .IsUnique(); + + b.ToTable("Artists"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.ArtistRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("ArtistId") + .HasColumnType("integer"); + + b.Property("ArtistRelationType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RelatedArtistId") + .HasColumnType("integer"); + + b.Property("RelationEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("RelationStart") + .HasColumnType("timestamp with time zone"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("RelatedArtistId"); + + b.HasIndex("ArtistId", "RelatedArtistId") + .IsUnique(); + + b.ToTable("ArtistRelation"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Bookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlternateNames") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("AmgId") + .HasColumnType("text"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CalculatedRating") + .HasColumnType("numeric"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeezerId") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("DiscogsId") + .HasColumnType("text"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("ItunesId") + .HasColumnType("text"); + + b.Property("LastFmId") + .HasColumnType("text"); + + b.Property("LastMetaDataUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPlayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MusicBrainzId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PlayedCount") + .HasColumnType("integer"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("SpotifyId") + .HasColumnType("text"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WikiDataId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("MusicBrainzId") + .IsUnique(); + + b.HasIndex("SongId"); + + b.HasIndex("SpotifyId") + .IsUnique(); + + b.HasIndex("UserId", "SongId") + .IsUnique(); + + b.ToTable("Bookmarks"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Chart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsGeneratedPlaylistEnabled") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("SourceName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SourceUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Charts"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.ChartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumTitle") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ArtistName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ChartId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LinkConfidence") + .HasColumnType("numeric"); + + b.Property("LinkNotes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("LinkStatus") + .HasColumnType("smallint"); + + b.Property("LinkedAlbumId") + .HasColumnType("integer"); + + b.Property("LinkedArtistId") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("ReleaseYear") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LinkedAlbumId"); + + b.HasIndex("LinkedArtistId"); + + b.HasIndex("ChartId", "LinkedAlbumId"); + + b.HasIndex("ChartId", "Rank") + .IsUnique(); + + b.ToTable("ChartItems"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Contributor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumId") + .HasColumnType("integer"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("ArtistId") + .HasColumnType("integer"); + + b.Property("ContributorName") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ContributorType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MetaTagIdentifier") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("SubRole") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("AlbumId"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("SongId"); + + b.HasIndex("ArtistId", "MetaTagIdentifier", "SongId") + .IsUnique(); + + b.HasIndex("ContributorName", "MetaTagIdentifier", "SongId") + .IsUnique(); + + b.ToTable("Contributors"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.JellyfinAccessToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Client") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Device") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("DeviceId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TokenPrefixHash") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("TokenSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Version") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("TokenPrefixHash"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "ExpiresAt", "RevokedAt"); + + b.ToTable("JellyfinAccessTokens"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.JobHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationInMs") + .HasColumnType("double precision"); + + b.Property("ErrorMessage") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("JobName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.Property("WasManualTrigger") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("StartedAt"); + + b.HasIndex("JobName", "StartedAt"); + + b.ToTable("JobHistories"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumCount") + .HasColumnType("integer"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("ArtistCount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastScanAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("SongCount") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("Type") + .IsUnique() + .HasFilter("\"Type\" != 3"); + + b.ToTable("Libraries"); + + b.HasData( + new + { + Id = 1, + ApiKey = new Guid("6d455bb8-7292-cba0-2fd0-c18e40ad8fc5"), + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Files in this directory are scanned and Album information is gathered via processing.", + IsLocked = false, + Name = "Inbound", + Path = "/app/inbound/", + SortOrder = 0, + Type = 1 + }, + new + { + Id = 2, + ApiKey = new Guid("020e8374-59db-6d77-bdf8-b308e278b48c"), + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "The staging directory to place processed files into (Inbound -> Staging -> Library).", + IsLocked = false, + Name = "Staging", + Path = "/app/staging/", + SortOrder = 0, + Type = 2 + }, + new + { + Id = 3, + ApiKey = new Guid("f63a6428-55d5-847b-3d09-3fa3b69b66ae"), + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "The library directory to place processed, reviewed and ready to use music files into.", + IsLocked = false, + Name = "Storage", + Path = "/app/storage/", + SortOrder = 0, + Type = 3 + }, + new + { + Id = 4, + ApiKey = new Guid("277e8907-d170-780d-816d-92111e007606"), + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Library where user images are stored.", + IsLocked = false, + Name = "User Images", + Path = "/app/user-images/", + SortOrder = 0, + Type = 4 + }, + new + { + Id = 5, + ApiKey = new Guid("4be2eea8-571d-6936-ecf6-5f99dd829c04"), + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Library where playlist data is stored.", + IsLocked = false, + Name = "Playlist Data", + Path = "/app/playlists/", + SortOrder = 0, + Type = 5 + }, + new + { + Id = 6, + ApiKey = new Guid("62453b56-402b-8f9e-073b-e2d31e9f7cf9"), + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Library where templates are stored, organized by language code.", + IsLocked = false, + Name = "Templates", + Path = "/app/templates/", + SortOrder = 0, + Type = 7 + }, + new + { + Id = 7, + ApiKey = new Guid("01d52713-b3cf-48fa-f085-7704baee6dc5"), + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Library where podcast media files are stored.", + IsLocked = false, + Name = "Podcasts", + Path = "/app/podcasts/", + SortOrder = 0, + Type = 8 + }, + new + { + Id = 8, + ApiKey = new Guid("f718b349-eccc-ff93-f992-c190e1ed2616"), + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Library where custom theme packs are stored.", + IsLocked = false, + Name = "Themes", + Path = "/app/themes/", + SortOrder = 0, + Type = 9 + }); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.LibraryScanHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationInMs") + .HasColumnType("double precision"); + + b.Property("ForAlbumId") + .HasColumnType("integer"); + + b.Property("ForArtistId") + .HasColumnType("integer"); + + b.Property("FoundAlbumsCount") + .HasColumnType("integer"); + + b.Property("FoundArtistsCount") + .HasColumnType("integer"); + + b.Property("FoundSongsCount") + .HasColumnType("integer"); + + b.Property("LibraryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryScanHistories"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartyAuditEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PartySessionId") + .HasColumnType("integer"); + + b.Property("PayloadJson") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("PartySessionId"); + + b.HasIndex("UserId"); + + b.ToTable("PartyAuditEvents"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartyPlaybackState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentQueueItemApiKey") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsPlaying") + .HasColumnType("boolean"); + + b.Property("LastHeartbeatAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PartySessionId") + .HasColumnType("integer"); + + b.Property("PositionSeconds") + .HasColumnType("double precision"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UpdatedByUserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("CurrentQueueItemApiKey"); + + b.HasIndex("IsPlaying"); + + b.HasIndex("LastHeartbeatAt"); + + b.HasIndex("PartySessionId") + .IsUnique(); + + b.HasIndex("UpdatedByUserId"); + + b.ToTable("PartyPlaybackStates"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartyQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("EnqueuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnqueuedByUserId") + .HasColumnType("integer"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PartySessionId") + .HasColumnType("integer"); + + b.Property("SongApiKey") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Source") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("EnqueuedAt"); + + b.HasIndex("EnqueuedByUserId"); + + b.HasIndex("SongApiKey"); + + b.HasIndex("PartySessionId", "SortOrder"); + + b.ToTable("PartyQueueItems"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartySession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveEndpointId") + .HasColumnType("uuid"); + + b.Property("ActiveEndpointId1") + .HasColumnType("integer"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsEndpointOffline") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsQueueLocked") + .HasColumnType("boolean"); + + b.Property("JoinCodeHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("OwnerUserId") + .HasColumnType("integer"); + + b.Property("PlaybackRevision") + .HasColumnType("bigint"); + + b.Property("QueueRevision") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ActiveEndpointId"); + + b.HasIndex("ActiveEndpointId1"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("OwnerUserId"); + + b.HasIndex("Status"); + + b.ToTable("PartySessions"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartySessionEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CapabilitiesJson") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsShared") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("OwnerUserId") + .HasColumnType("integer"); + + b.Property("Room") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("IsShared"); + + b.HasIndex("LastSeenAt"); + + b.HasIndex("OwnerUserId"); + + b.HasIndex("Room"); + + b.HasIndex("Type"); + + b.ToTable("PartySessionEndpoints"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartySessionParticipant", b => + { + b.Property("PartySessionId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("PartySessionId", "UserId"); + + b.HasIndex("IsBanned"); + + b.HasIndex("Role"); + + b.HasIndex("UserId"); + + b.HasIndex("PartySessionId", "UserId") + .IsUnique(); + + b.ToTable("PartySessionParticipants"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlayQueue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("ChangedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsCurrentSong") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PlayQueId") + .HasColumnType("integer"); + + b.Property("Position") + .HasColumnType("double precision"); + + b.Property("SongApiKey") + .HasColumnType("uuid"); + + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("SongId"); + + b.HasIndex("UserId"); + + b.ToTable("PlayQues"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("Hostname") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IpAddress") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxBitRate") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ScrobbleEnabled") + .HasColumnType("boolean"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("TranscodingId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UserAgent") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("UserId", "Client", "UserAgent"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedUserIds") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("Comment") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("Duration") + .HasColumnType("double precision"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsPublic") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PlaylistUploadedFileId") + .HasColumnType("integer"); + + b.Property("SongCount") + .HasColumnType("smallint"); + + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("SourceType") + .HasColumnType("smallint"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("PlaylistUploadedFileId"); + + b.HasIndex("SongId"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistSong", b => + { + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("PlaylistId") + .HasColumnType("integer"); + + b.Property("PlaylistOrder") + .HasColumnType("integer"); + + b.Property("SongApiKey") + .HasColumnType("uuid"); + + b.HasKey("SongId", "PlaylistId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("SongId", "PlaylistId") + .IsUnique(); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("FileData") + .HasColumnType("bytea"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("PlaylistUploadedFiles"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFileItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HintsJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("LastAttemptUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedReference") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("PlaylistUploadedFileId") + .HasColumnType("integer"); + + b.Property("RawReference") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("SongId"); + + b.HasIndex("PlaylistUploadedFileId", "SortOrder"); + + b.ToTable("PlaylistUploadedFileItems"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PodcastChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("AutoDownloadEnabled") + .HasColumnType("boolean"); + + b.Property("ConsecutiveFailureCount") + .HasColumnType("integer"); + + b.Property("CoverArtLocalPath") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("Etag") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("FeedUrl") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ImageUrl") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSyncAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSyncAttemptAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSyncError") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxDownloadedEpisodes") + .HasColumnType("integer"); + + b.Property("MaxStorageBytes") + .HasColumnType("bigint"); + + b.Property("NextSyncAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RefreshIntervalHours") + .HasColumnType("integer"); + + b.Property("SiteUrl") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TitleNormalized") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("IsDeleted"); + + b.HasIndex("NextSyncAt"); + + b.HasIndex("UserId", "FeedUrl") + .IsUnique(); + + b.ToTable("PodcastChannels"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PodcastEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("DownloadError") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("DownloadStatus") + .HasColumnType("integer"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("EnclosureLength") + .HasColumnType("bigint"); + + b.Property("EnclosureUrl") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EpisodeKey") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Guid") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LocalFileSize") + .HasColumnType("bigint"); + + b.Property("LocalPath") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("MimeType") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PodcastChannelId") + .HasColumnType("integer"); + + b.Property("PublishDate") + .HasColumnType("timestamp with time zone"); + + b.Property("QueuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TitleNormalized") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("PodcastChannelId", "DownloadStatus"); + + b.HasIndex("PodcastChannelId", "EpisodeKey") + .IsUnique(); + + b.HasIndex("PodcastChannelId", "PublishDate"); + + b.ToTable("PodcastEpisodes"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PodcastEpisodeBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PodcastEpisodeId") + .HasColumnType("integer"); + + b.Property("PositionSeconds") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PodcastEpisodeId"); + + b.HasIndex("UserId", "PodcastEpisodeId") + .IsUnique(); + + b.ToTable("PodcastEpisodeBookmarks"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RadioStation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("HomePageUrl") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StreamUrl") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.ToTable("RadioStations"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("DeviceId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HashedToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IpAddress") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ReplacedByToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedReason") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SessionStartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("TokenFamily") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserAgent") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("HashedToken") + .IsUnique(); + + b.HasIndex("TokenFamily"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumTitle") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AlbumTitleNormalized") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("ArtistName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ArtistNameNormalized") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("integer"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("DescriptionNormalized") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("ExternalUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("LastActivityAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastActivityType") + .HasColumnType("integer"); + + b.Property("LastActivityUserId") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("ReleaseYear") + .HasColumnType("integer"); + + b.Property("SongTitle") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SongTitleNormalized") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TargetAlbumApiKey") + .HasColumnType("uuid"); + + b.Property("TargetArtistApiKey") + .HasColumnType("uuid"); + + b.Property("TargetSongApiKey") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("LastActivityUserId"); + + b.HasIndex("UpdatedByUserId"); + + b.HasIndex("CreatedAt", "Id") + .IsDescending(); + + b.HasIndex("LastActivityAt", "Id") + .IsDescending(); + + b.HasIndex("CreatedByUserId", "CreatedAt", "Id") + .IsDescending(false, true, true); + + b.HasIndex("Status", "CreatedAt", "Id") + .IsDescending(false, true, true); + + b.HasIndex("TargetAlbumApiKey", "CreatedAt", "Id") + .IsDescending(false, true, true); + + b.HasIndex("TargetArtistApiKey", "CreatedAt", "Id") + .IsDescending(false, true, true); + + b.HasIndex("Status", "CreatedByUserId", "CreatedAt", "Id") + .IsDescending(false, false, true, true); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RequestComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("integer"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("ParentCommentId") + .HasColumnType("integer"); + + b.Property("RequestId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("RequestId", "CreatedAt", "Id"); + + b.HasIndex("RequestId", "ParentCommentId", "CreatedAt", "Id"); + + b.ToTable("RequestComments"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RequestParticipant", b => + { + b.Property("RequestId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCommenter") + .HasColumnType("boolean"); + + b.Property("IsCreator") + .HasColumnType("boolean"); + + b.HasKey("RequestId", "UserId"); + + b.HasIndex("UserId", "RequestId"); + + b.ToTable("RequestParticipants"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RequestUserState", b => + { + b.Property("RequestId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RequestId", "UserId"); + + b.HasIndex("UserId", "LastSeenAt"); + + b.ToTable("RequestUserStates"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.SearchHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ByUserAgent") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ByUserId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FoundAlbumsCount") + .HasColumnType("integer"); + + b.Property("FoundArtistsCount") + .HasColumnType("integer"); + + b.Property("FoundOtherItems") + .HasColumnType("integer"); + + b.Property("FoundSongsCount") + .HasColumnType("integer"); + + b.Property("SearchDurationInMs") + .HasColumnType("double precision"); + + b.Property("SearchQuery") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.HasKey("Id"); + + b.ToTable("SearchHistories"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("Comment") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("Category"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Settings"); + + b.HasData( + new + { + Id = 1, + ApiKey = new Guid("5c08b275-6c25-972d-2aef-7e2f6ba227f2"), + Comment = "Add a default filter to show only albums with this or less number of songs.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "filtering.lessThanSongCount", + SortOrder = 0, + Value = "3" + }, + new + { + Id = 2, + ApiKey = new Guid("c4996dec-2489-820e-eb83-6ddbd1144557"), + Comment = "Add a default filter to show only albums with this or less duration.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "filtering.lessThanDuration", + SortOrder = 0, + Value = "720000" + }, + new + { + Id = 4, + ApiKey = new Guid("9a803c96-ca09-9208-d9e6-04083a5a11ea"), + Comment = "Default page size when view including pagination.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "defaults.pagesize", + SortOrder = 0, + Value = "100" + }, + new + { + Id = 6, + ApiKey = new Guid("6b5c2528-7420-0e22-f136-6db9b89d9d7e"), + Comment = "Amount of time to display a Toast then auto-close (in milliseconds.)", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "userinterface.toastAutoCloseTime", + SortOrder = 0, + Value = "2000" + }, + new + { + Id = 300, + ApiKey = new Guid("318f1b81-ec0f-a6c6-05e0-805f67b8caab"), + Category = 3, + Comment = "Short Format to use when displaying full dates.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "formatting.dateTimeDisplayFormatShort", + SortOrder = 0, + Value = "yyyyMMdd HH\\:mm" + }, + new + { + Id = 301, + ApiKey = new Guid("3a06decd-3d51-f70b-c0ac-d640e8bd6f40"), + Category = 3, + Comment = "Format to use when displaying activity related dates (e.g., processing messages)", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "formatting.dateTimeDisplayActivityFormat", + SortOrder = 0, + Value = "hh\\:mm\\:ss\\.ffff" + }, + new + { + Id = 9, + ApiKey = new Guid("56a687bc-652d-9128-d7fd-52125c518a1c"), + Comment = "List of ignored articles when scanning media (pipe delimited).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.ignoredArticles", + SortOrder = 0, + Value = "THE|EL|LA|LOS|LAS|LE|LES|OS|AS|O|A" + }, + new + { + Id = 500, + ApiKey = new Guid("2ebd9e4b-a639-f66a-0574-69d765fa4a07"), + Category = 5, + Comment = "Is Magic processing enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "magic.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 501, + ApiKey = new Guid("bd081306-fb20-dbb6-c886-da6a42b080af"), + Category = 5, + Comment = "Renumber songs when doing magic processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "magic.doRenumberSongs", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 502, + ApiKey = new Guid("13bde2a9-4729-31d3-5fbf-6e0ab74437a0"), + Category = 5, + Comment = "Remove featured artists from song artist when doing magic.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "magic.doRemoveFeaturingArtistFromSongArtist", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 503, + ApiKey = new Guid("c5221bbc-e459-1944-cf36-b874dd93247c"), + Category = 5, + Comment = "Remove featured artists from song title when doing magic.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "magic.doRemoveFeaturingArtistFromSongTitle", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 504, + ApiKey = new Guid("30e02344-8dec-c2ea-d203-22a803f93b48"), + Category = 5, + Comment = "Replace song artist separators with standard ID3 separator ('/') when doing magic.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "magic.doReplaceSongsArtistSeparators", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 505, + ApiKey = new Guid("163cf2d8-cb34-8509-0df3-8b681a0ae74b"), + Category = 5, + Comment = "Set the song year to current year if invalid or missing when doing magic.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "magic.doSetYearToCurrentIfInvalid", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 506, + ApiKey = new Guid("616cc758-2766-8f2f-71ae-2f99b98aba63"), + Category = 5, + Comment = "Remove unwanted text from album title when doing magic.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "magic.doRemoveUnwantedTextFromAlbumTitle", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 507, + ApiKey = new Guid("b9afe726-36f8-0b50-3a3d-a6eeb53b8e37"), + Category = 5, + Comment = "Remove unwanted text from song titles when doing magic.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "magic.doRemoveUnwantedTextFromSongTitles", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 200, + ApiKey = new Guid("e0a0ca63-aeb9-650e-99c4-d95a791c4a2e"), + Category = 2, + Comment = "Enable Melodee to convert non-mp3 media files during processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "conversion.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 201, + ApiKey = new Guid("5025f51c-262d-e7c5-ad27-70bddf43b476"), + Category = 2, + Comment = "Bitrate to convert non-mp3 media files during processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "conversion.bitrate", + SortOrder = 0, + Value = "384" + }, + new + { + Id = 202, + ApiKey = new Guid("92cbee43-6e9f-a236-a271-f9cc5bb5d262"), + Category = 2, + Comment = "Vbr to convert non-mp3 media files during processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "conversion.vbrLevel", + SortOrder = 0, + Value = "4" + }, + new + { + Id = 203, + ApiKey = new Guid("f88fb399-23c1-ef86-3e56-93f63f8bb809"), + Category = 2, + Comment = "Sampling rate to convert non-mp3 media files during processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "conversion.samplingRate", + SortOrder = 0, + Value = "48000" + }, + new + { + Id = 700, + ApiKey = new Guid("8ccfdf94-55f8-bd0e-cb7c-8052d6d2ca89"), + Category = 7, + Comment = "Process of CueSheet files during processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "plugin.cueSheet.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 701, + ApiKey = new Guid("9edd4162-4e67-68e5-67e6-65a023fa3d41"), + Category = 7, + Comment = "Process of M3U files during processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "plugin.m3u.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 702, + ApiKey = new Guid("cd93553f-b424-dd6d-00da-1fd3de10267c"), + Category = 7, + Comment = "Process of NFO files during processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "plugin.nfo.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 703, + ApiKey = new Guid("cffd7f2e-95f3-28a2-e315-699f413b13ff"), + Category = 7, + Comment = "Process of Simple File Verification (SFV) files during processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "plugin.simpleFileVerification.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 704, + ApiKey = new Guid("50894ac8-809a-d90f-79ef-8169b16b0296"), + Category = 7, + Comment = "If true then all comments will be removed from media files.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.doDeleteComments", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 26, + ApiKey = new Guid("cf595b62-3932-5723-49f3-1eba81bbf147"), + Comment = "Fragments of artist names to replace (JSON Dictionary).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.artistNameReplacements", + SortOrder = 0, + Value = "{'AC/DC': ['AC; DC', 'AC;DC', 'AC/ DC', 'AC DC'] , 'Love/Hate': ['Love; Hate', 'Love;Hate', 'Love/ Hate', 'Love Hate'] }" + }, + new + { + Id = 27, + ApiKey = new Guid("fd8eb2e5-9d1d-95ad-93e3-4129f18ca952"), + Comment = "If OrigAlbumYear [TOR, TORY, TDOR] value is invalid use current year.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.doUseCurrentYearAsDefaultOrigAlbumYearValue", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 28, + ApiKey = new Guid("286bf3c1-9d25-a8ce-d78d-964db9d15b37"), + Comment = "Delete original files when processing. When false a copy if made, else original is deleted after processed.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.doDeleteOriginal", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 29, + ApiKey = new Guid("4f830df7-7942-6353-1d84-946f271c084e"), + Comment = "Extension to add to file when converted, leave blank to disable.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.convertedExtension", + SortOrder = 0, + Value = "_converted" + }, + new + { + Id = 30, + ApiKey = new Guid("d2e7b90f-8c28-863f-f96f-14627ac06394"), + Comment = "Extension to add to file when processed, leave blank to disable.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.processedExtension", + SortOrder = 0, + Value = "_processed" + }, + new + { + Id = 32, + ApiKey = new Guid("1e80ad9a-a13e-b515-9262-1c0dd6e51bb9"), + Comment = "When processing over write any existing Melodee data files, otherwise skip and leave in place.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.doOverrideExistingMelodeeDataFiles", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 34, + ApiKey = new Guid("7d283a60-e2c1-e3f3-6b1f-3c988a89cfc9"), + Comment = "The maximum number of files to process, set to zero for unlimited.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.maximumProcessingCount", + SortOrder = 0, + Value = "0" + }, + new + { + Id = 35, + ApiKey = new Guid("2277af16-56ba-327d-44d4-3f1e1dba4366"), + Comment = "Maximum allowed length of album directory name.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.maximumAlbumDirectoryNameLength", + SortOrder = 0, + Value = "255" + }, + new + { + Id = 36, + ApiKey = new Guid("9ebc2634-b7d3-12c4-3487-606d1ed8d376"), + Comment = "Maximum allowed length of artist directory name.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.maximumArtistDirectoryNameLength", + SortOrder = 0, + Value = "255" + }, + new + { + Id = 37, + ApiKey = new Guid("a4f7e266-d355-e402-865f-da369963cc03"), + Comment = "Fragments to remove from album titles (JSON array).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.albumTitleRemovals", + SortOrder = 0, + Value = "['^', '~', '#']" + }, + new + { + Id = 38, + ApiKey = new Guid("f29aff69-bc10-d860-692e-275a4ffa4138"), + Comment = "Fragments to remove from song titles (JSON array).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.songTitleRemovals", + SortOrder = 0, + Value = "[';', '(Remaster)', 'Remaster']" + }, + new + { + Id = 39, + ApiKey = new Guid("4585dcb2-e48c-b99a-8995-91f56931e11e"), + Comment = "Continue processing if an error is encountered.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.doContinueOnDirectoryProcessingErrors", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 41, + ApiKey = new Guid("02088d3e-a9d2-44a4-0975-41c1f695ebdb"), + Comment = "Is scripting enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "scripting.enabled", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 42, + ApiKey = new Guid("262c50a8-e2a9-53d6-2bce-82d075d843ec"), + Comment = "Script to run before processing the inbound directory, leave blank to disable.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "scripting.preDiscoveryScript", + SortOrder = 0, + Value = "" + }, + new + { + Id = 43, + ApiKey = new Guid("e999453e-9193-fbfe-a533-ab541773943e"), + Comment = "Script to run after processing the inbound directory, leave blank to disable.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "scripting.postDiscoveryScript", + SortOrder = 0, + Value = "" + }, + new + { + Id = 45, + ApiKey = new Guid("5f2c94f9-dfb3-2e40-06b1-9dd70a9f9f62"), + Comment = "Don't create performer contributors for these performer names.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.ignoredPerformers", + SortOrder = 0, + Value = "" + }, + new + { + Id = 46, + ApiKey = new Guid("443fb612-30f1-1b13-4903-ad55009dceac"), + Comment = "Don't create production contributors for these production names.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.ignoredProduction", + SortOrder = 0, + Value = "['www.t.me;pmedia_music']" + }, + new + { + Id = 47, + ApiKey = new Guid("7beaf728-5c50-dabd-5ec2-f5a5138c0822"), + Comment = "Don't create publisher contributors for these artist names.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.ignoredPublishers", + SortOrder = 0, + Value = "['P.M.E.D.I.A','PMEDIA','PMEDIA GROUP']" + }, + new + { + Id = 49, + ApiKey = new Guid("44b73f87-3a4a-c6d2-e3cf-b37ea7937563"), + Comment = "Private key used to encrypt/decrypt passwords for Subsonic authentication. Use https://generate-random.org/encryption-key-generator?count=1&bytes=32&cipher=aes-256-cbc&string=&password= to generate a new key.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "encryption.privateKey", + SortOrder = 0, + Value = "H+Kiik6VMKfTD2MesF1GoMjczTrD5RhuKckJ5+/UQWOdWajGcsEC3yEnlJ5eoy8Y" + }, + new + { + Id = 50, + ApiKey = new Guid("582676cf-cf72-3c09-1055-5a3b2de29a6d"), + Comment = "Prefix to apply to indicate an album directory is a duplicate album for an artist. If left blank the default of '__duplicate_' will be used.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.duplicateAlbumPrefix", + SortOrder = 0, + Value = "_duplicate_ " + }, + new + { + Id = 1300, + ApiKey = new Guid("3ff6d2e5-dd61-c1de-c556-0a8f1169aa43"), + Category = 13, + Comment = "The maximum value a song number can have for an album.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "validation.maximumSongNumber", + SortOrder = 0, + Value = "9999" + }, + new + { + Id = 1301, + ApiKey = new Guid("70f56e2f-1c9a-05dc-7da7-c6347e3f1947"), + Category = 13, + Comment = "Minimum allowed year for an album.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "validation.minimumAlbumYear", + SortOrder = 0, + Value = "1860" + }, + new + { + Id = 1302, + ApiKey = new Guid("b257b1e3-3731-c980-137d-c4d0197753ce"), + Category = 13, + Comment = "Maximum allowed year for an album.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "validation.maximumAlbumYear", + SortOrder = 0, + Value = "2150" + }, + new + { + Id = 1303, + ApiKey = new Guid("b9fe8d2e-01b4-ed09-7d3a-23cfdd6ba221"), + Category = 13, + Comment = "Minimum number of songs an album has to have to be considered valid, set to 0 to disable check.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "validation.minimumSongCount", + SortOrder = 0, + Value = "3" + }, + new + { + Id = 1304, + ApiKey = new Guid("d9b766a1-cf5f-a185-028b-8303ecb12b4a"), + Category = 13, + Comment = "Minimum duration of an album to be considered valid (in minutes), set to 0 to disable check.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "validation.minimumAlbumDuration", + SortOrder = 0, + Value = "10" + }, + new + { + Id = 100, + ApiKey = new Guid("a4c47b7c-30c3-0603-cf8e-79863111f251"), + Category = 1, + Comment = "OpenSubsonic server supported Subsonic API version.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "openSubsonicServer.openSubsonic.serverSupportedVersion", + SortOrder = 0, + Value = "1.16.1" + }, + new + { + Id = 101, + ApiKey = new Guid("5a954c6a-9afc-43eb-8f93-74047d725365"), + Category = 1, + Comment = "OpenSubsonic server name.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "openSubsonicServer.openSubsonicServer.type", + SortOrder = 0, + Value = "Melodee" + }, + new + { + Id = 103, + ApiKey = new Guid("95256bc3-92e8-a83e-e26d-b643d93d621a"), + Category = 1, + Comment = "OpenSubsonic email to use in License responses.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "openSubsonicServer.openSubsonicServerLicenseEmail", + SortOrder = 0, + Value = "noreply@localhost.lan" + }, + new + { + Id = 104, + ApiKey = new Guid("8f6dca18-fe45-9659-260b-41dd9a66cbf3"), + Category = 1, + Comment = "Limit the number of artists to include in an indexes request, set to zero for 32k per index (really not recommended with tens of thousands of artists and mobile clients timeout downloading indexes, a user can find an artist by search)", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "openSubsonicServer.openSubsonicServer.index.artistLimit", + SortOrder = 0, + Value = "1000" + }, + new + { + Id = 53, + ApiKey = new Guid("b48052d3-aab1-dc24-9188-17617fc90575"), + Comment = "Processing batching size. Allowed range is between [250] and [1000]. ", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "defaults.batchSize", + SortOrder = 0, + Value = "250" + }, + new + { + Id = 54, + ApiKey = new Guid("7464b039-de31-f876-5731-46ce62500117"), + Comment = "When processing folders immediately delete any files with these extensions. (JSON array).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "processing.fileExtensionsToDelete", + SortOrder = 0, + Value = "['log', 'lnk', 'lrc', 'doc']" + }, + new + { + Id = 902, + ApiKey = new Guid("1ff4eed4-1cc5-d453-6ee5-947784437a60"), + Category = 9, + Comment = "User agent to send with Search engine requests.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.userAgent", + SortOrder = 0, + Value = "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0" + }, + new + { + Id = 903, + ApiKey = new Guid("b233a0ac-9743-0b2b-1055-014c23f4147f"), + Category = 9, + Comment = "Default page size when performing a search engine search.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.defaultPageSize", + SortOrder = 0, + Value = "20" + }, + new + { + Id = 904, + ApiKey = new Guid("cec2c46f-97dd-347a-53ea-c2b8a8ee6bf2"), + Category = 9, + Comment = "Is MusicBrainz search engine enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.musicbrainz.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 905, + ApiKey = new Guid("798d3376-ff64-b590-f204-c46bef35339a"), + Category = 9, + Comment = "Storage path to hold MusicBrainz downloaded files and SQLite db.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.musicbrainz.storagePath", + SortOrder = 0, + Value = "/melodee_test/search-engine-storage/musicbrainz/" + }, + new + { + Id = 906, + ApiKey = new Guid("2fbfdf98-8a93-ded3-1eed-4582f6ec2dc6"), + Category = 9, + Comment = "Maximum number of batches import from MusicBrainz downloaded db dump (this setting is usually used during debugging), set to zero for unlimited.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.musicbrainz.importMaximumToProcess", + SortOrder = 0, + Value = "0" + }, + new + { + Id = 907, + ApiKey = new Guid("fb35de56-6659-1268-9f28-97e0be7d870c"), + Category = 9, + Comment = "Number of records to import from MusicBrainz downloaded db dump before commiting to local SQLite database.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.musicbrainz.importBatchSize", + SortOrder = 0, + Value = "50000" + }, + new + { + Id = 908, + ApiKey = new Guid("f5f8842b-1294-e4ab-95e1-2b60fa955b09"), + Category = 9, + Comment = "Timestamp of when last MusicBrainz import was successful.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.musicbrainz.importLastImportTimestamp", + SortOrder = 0, + Value = "" + }, + new + { + Id = 910, + ApiKey = new Guid("1546df1d-4e92-2d14-9092-44d6daeb689e"), + Category = 9, + Comment = "Is Spotify search engine enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.spotify.enabled", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 911, + ApiKey = new Guid("e11913ea-3d25-8024-c207-30837c59fee1"), + Category = 9, + Comment = "ApiKey used used with Spotify. See https://developer.spotify.com/ for more details.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.spotify.apiKey", + SortOrder = 0, + Value = "" + }, + new + { + Id = 912, + ApiKey = new Guid("0c683b52-4b31-ea62-1421-f895264e8b29"), + Category = 9, + Comment = "Shared secret used with Spotify. See https://developer.spotify.com/ for more details.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.spotify.sharedSecret", + SortOrder = 0, + Value = "" + }, + new + { + Id = 913, + ApiKey = new Guid("7c9b3a2a-91ad-0f5a-cca2-d2a9ab7f4379"), + Category = 9, + Comment = "Token obtained from Spotify using the ApiKey and the Secret, this json contains expiry information.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.spotify.accessToken", + SortOrder = 0, + Value = "" + }, + new + { + Id = 914, + ApiKey = new Guid("4a089459-cc6b-d516-42c3-22ead8d2c7ac"), + Category = 9, + Comment = "Is ITunes search engine enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.itunes.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 915, + ApiKey = new Guid("b63db7ba-321a-46a2-7e6a-8dc75313945f"), + Category = 9, + Comment = "Is LastFM search engine enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.lastFm.Enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 916, + ApiKey = new Guid("6c1087d4-e491-5a75-293d-c80ba2e59acb"), + Category = 9, + Comment = "When performing a search engine search, the maximum allowed page size.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.maximumAllowedPageSize", + SortOrder = 0, + Value = "1000" + }, + new + { + Id = 917, + ApiKey = new Guid("a9dddd78-8c93-9f48-fe2c-7d6cd303c32f"), + Category = 9, + Comment = "Refresh albums for artists from search engine database every x days, set to zero to not refresh.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.artistSearchDatabaseRefreshInDays", + SortOrder = 0, + Value = "14" + }, + new + { + Id = 918, + ApiKey = new Guid("dfc917eb-2be2-6a79-2f66-8fba157d5778"), + Category = 9, + Comment = "Is Deezer search engine enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.deezer.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 919, + ApiKey = new Guid("de923cf1-09d4-8a9d-14a2-d4dda9eb8556"), + Category = 9, + Comment = "Is Metal API search engine enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.metalApi.enabled", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 400, + ApiKey = new Guid("5dbf9b93-4c1f-e317-37ed-97b3e641772c"), + Category = 4, + Comment = "Include any embedded images from media files into the Melodee data file.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "imaging.doLoadEmbeddedImages", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 401, + ApiKey = new Guid("8425f968-cb8a-a4bc-3174-a0b07641102e"), + Category = 4, + Comment = "Small image size (square image, this is both width and height).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "imaging.smallSize", + SortOrder = 0, + Value = "300" + }, + new + { + Id = 402, + ApiKey = new Guid("6261b063-df52-a8b2-70f7-9619312364d2"), + Category = 4, + Comment = "Medium image size (square image, this is both width and height).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "imaging.mediumSize", + SortOrder = 0, + Value = "600" + }, + new + { + Id = 403, + ApiKey = new Guid("f9d91f6b-172c-e91f-6c90-5257aa9e3e01"), + Category = 4, + Comment = "Large image size (square image, this is both width and height), if larger than will be resized to this image, leave blank to disable.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "imaging.largeSize", + SortOrder = 0, + Value = "1600" + }, + new + { + Id = 404, + ApiKey = new Guid("08a6111e-0d45-a09c-86e6-979cd47183be"), + Category = 4, + Comment = "Maximum allowed number of images for an album, this includes all image types (Front, Rear, etc.), set to zero for unlimited.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "imaging.maximumNumberOfAlbumImages", + SortOrder = 0, + Value = "25" + }, + new + { + Id = 405, + ApiKey = new Guid("9320ee39-2c29-9fb3-1269-cf38f6cf32d3"), + Category = 4, + Comment = "Maximum allowed number of images for an artist, set to zero for unlimited.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "imaging.maximumNumberOfArtistImages", + SortOrder = 0, + Value = "25" + }, + new + { + Id = 406, + ApiKey = new Guid("c0d392bc-7142-5407-4e11-a1f2c6d8eb55"), + Category = 4, + Comment = "Images under this size are considered invalid, set to zero to disable.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "imaging.minimumImageSize", + SortOrder = 0, + Value = "300" + }, + new + { + Id = 1200, + ApiKey = new Guid("e0cefa09-426a-e3dd-a65a-498708d55e72"), + Category = 12, + Comment = "Default format for transcoding.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "transcoding.default", + SortOrder = 0, + Value = "raw" + }, + new + { + Id = 1201, + ApiKey = new Guid("e2be036e-1bfa-44bb-c8ee-abb86ba87fbf"), + Category = 12, + Comment = "Default command to transcode MP3 for streaming.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "transcoding.command.mp3", + SortOrder = 0, + Value = "{ 'format': 'Mp3', 'bitrate: 192, 'command': 'ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -f mp3 -' }" + }, + new + { + Id = 1202, + ApiKey = new Guid("17e73900-e7f3-a01b-2710-cbc01e43f7c5"), + Category = 12, + Comment = "Default command to transcode using libopus for streaming.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "transcoding.command.opus", + SortOrder = 0, + Value = "{ 'format': 'Opus', 'bitrate: 128, 'command': 'ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a libopus -f opus -' }" + }, + new + { + Id = 1203, + ApiKey = new Guid("f160bbd0-5316-bf0e-2d20-498426f48241"), + Category = 12, + Comment = "Default command to transcode to aac for streaming.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "transcoding.command.aac", + SortOrder = 0, + Value = "{ 'format': 'Aac', 'bitrate: 256, 'command': 'ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -' }" + }, + new + { + Id = 1000, + ApiKey = new Guid("26666288-7cc7-7af2-3404-8e026f1cb6a7"), + Category = 10, + Comment = "Is scrobbling enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "scrobbling.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 1001, + ApiKey = new Guid("8d90f3ba-2a9d-9f11-e8e9-684e2d1c013d"), + Category = 10, + Comment = "Is scrobbling to Last.fm enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "scrobbling.lastFm.Enabled", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 1002, + ApiKey = new Guid("d0716532-ca01-997a-75e1-45ca0b56e999"), + Category = 10, + Comment = "ApiKey used used with last FM. See https://www.last.fm/api/authentication for more details.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "scrobbling.lastFm.apiKey", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1003, + ApiKey = new Guid("244b20d4-551f-dd7e-fd6c-81caefa013e7"), + Category = 10, + Comment = "Shared secret used with last FM. See https://www.last.fm/api/authentication for more details.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "scrobbling.lastFm.sharedSecret", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1100, + ApiKey = new Guid("84de96d4-42f4-1056-b509-d68d5ded3457"), + Category = 11, + Comment = "Base URL for Melodee to use when building shareable links and image urls (e.g., 'https://server.domain.com:8080', 'http://server.domain.com').", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "system.baseUrl", + SortOrder = 0, + Value = "** REQUIRED: THIS MUST BE EDITED **" + }, + new + { + Id = 1103, + ApiKey = new Guid("9468bf96-8fea-8dfb-c1a9-7b764c5178c6"), + Category = 11, + Comment = "Name for this Melodee instance (used in emails and UI branding).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Customize the display name of your Melodee instance. Defaults to 'Melodee' if not set.", + IsLocked = false, + Key = "system.siteName", + SortOrder = 0, + Value = "Melodee" + }, + new + { + Id = 1101, + ApiKey = new Guid("42a71bd4-6390-1880-cd7c-e5e19a4092b1"), + Category = 11, + Comment = "Is downloading enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "system.isDownloadingEnabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 1102, + ApiKey = new Guid("79457a59-de2d-667d-2813-a79cd70427cc"), + Category = 11, + Comment = "Maximum upload size in bytes for UI uploads.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "system.maxUploadSize", + SortOrder = 0, + Value = "5242880" + }, + new + { + Id = 1400, + ApiKey = new Guid("a6bc32c4-deb2-21c3-b5a9-0aa463d6247a"), + Category = 14, + Comment = "Cron expression to run the artist housekeeping job, set empty to disable. Default of '0 0 0/1 1/1 * ? *' will run every hour. See https://www.freeformatter.com/cron-expression-generator-quartz.html", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.artistHousekeeping.cronExpression", + SortOrder = 0, + Value = "0 0 0/1 1/1 * ? *" + }, + new + { + Id = 1401, + ApiKey = new Guid("5ef2d5be-debf-facc-6a06-0055acb63c74"), + Category = 14, + Comment = "Cron expression to run the library process job, set empty to disable. Default of '0 */10 * ? * *' Every 10 minutes. See https://www.freeformatter.com/cron-expression-generator-quartz.html", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.libraryProcess.cronExpression", + SortOrder = 0, + Value = "0 */10 * ? * *" + }, + new + { + Id = 1402, + ApiKey = new Guid("67dc3cad-e46b-ad78-c9bc-25a65e487114"), + Category = 14, + Comment = "Cron expression to run the library scan job, set empty to disable. Default of '0 0 0 * * ?' will run every day at 00:00. See https://www.freeformatter.com/cron-expression-generator-quartz.html", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.libraryInsert.cronExpression", + SortOrder = 0, + Value = "0 0 0 * * ?" + }, + new + { + Id = 1403, + ApiKey = new Guid("fab2408d-06d8-5ba8-78ff-db4b8d0a5c58"), + Category = 14, + Comment = "Cron expression to run the musicbrainz database house keeping job, set empty to disable. Default of '0 0 12 1 * ?' will run first day of the month. See https://www.freeformatter.com/cron-expression-generator-quartz.html", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.musicbrainzUpdateDatabase.cronExpression", + SortOrder = 0, + Value = "0 0 12 1 * ?" + }, + new + { + Id = 1404, + ApiKey = new Guid("219f3b33-dc1f-b3c2-143c-582a023e5b25"), + Category = 14, + Comment = "Cron expression to run the artist search engine house keeping job, set empty to disable. Default of '0 0 0 * * ?' will run every day at 00:00. See https://www.freeformatter.com/cron-expression-generator-quartz.html", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.artistSearchEngineHousekeeping.cronExpression", + SortOrder = 0, + Value = "0 0 0 * * ?" + }, + new + { + Id = 1405, + ApiKey = new Guid("c3f25109-36ca-e223-69a9-71a3d4083f00"), + Category = 14, + Comment = "Cron expression to run the chart update job which links chart items to albums, set empty to disable. Default of '0 0 2 * * ?' will run every day at 02:00. See https://www.freeformatter.com/cron-expression-generator-quartz.html", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.chartUpdate.cronExpression", + SortOrder = 0, + Value = "0 0 2 * * ?" + }, + new + { + Id = 1406, + ApiKey = new Guid("dcf2a737-2724-2310-abec-6d0204ff4bff"), + Category = 14, + Comment = "Cron expression for staging auto-move job. Moves 'Ok' albums to storage. Default '0 */15 * * * ?' runs every 15 min. Also triggered after inbound processing.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.stagingAutoMove.cronExpression", + SortOrder = 0, + Value = "0 */15 * * * ?" + }, + new + { + Id = 1500, + ApiKey = new Guid("77c527bc-5317-46da-d778-e7114791749f"), + Comment = "Enable or disable email sending functionality", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "When true, enables SMTP email sending for password resets and notifications", + IsLocked = false, + Key = "email.enabled", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 1501, + ApiKey = new Guid("1836553b-06a0-2fe4-35c0-fdf088520e61"), + Comment = "Display name in From field of outgoing emails", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "email.fromName", + SortOrder = 0, + Value = "Melodee" + }, + new + { + Id = 1502, + ApiKey = new Guid("28ce7a91-9dd3-bcdb-7cf2-2249037ff4a5"), + Comment = "Email address in From field (REQUIRED for email sending)", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Example: noreply@yourdomain.com", + IsLocked = false, + Key = "email.fromEmail", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1503, + ApiKey = new Guid("100f5f84-1a12-8af4-1b43-349bfea18d90"), + Comment = "SMTP server hostname (REQUIRED for email sending)", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Example: smtp.gmail.com or smtp.sendgrid.net", + IsLocked = false, + Key = "email.smtpHost", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1504, + ApiKey = new Guid("0f9b5ef0-1b03-2319-7e19-5fc2e9e7287d"), + Comment = "SMTP server port", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Common values: 587 (StartTLS), 465 (SSL), 25 (unencrypted)", + IsLocked = false, + Key = "email.smtpPort", + SortOrder = 0, + Value = "587" + }, + new + { + Id = 1505, + ApiKey = new Guid("41c53bd6-7fd6-bd69-673c-e352fa5f84a5"), + Comment = "SMTP authentication username (optional)", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Leave empty if SMTP server does not require authentication", + IsLocked = false, + Key = "email.smtpUsername", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1506, + ApiKey = new Guid("893a9053-2b8f-8a32-4e6c-c9b3541341db"), + Comment = "SMTP authentication password (optional, use env var email_smtpPassword)", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "For security, set via environment variable: email_smtpPassword", + IsLocked = false, + Key = "email.smtpPassword", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1507, + ApiKey = new Guid("9a20a527-a2d9-628f-914a-c2fab2dc8496"), + Comment = "Use SSL connection for SMTP", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Set to true for port 465 (SSL), false for port 587 (StartTLS)", + IsLocked = false, + Key = "email.smtpUseSsl", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 1508, + ApiKey = new Guid("1f6249d4-fb89-6266-9672-41d7a6109260"), + Comment = "Use StartTLS for SMTP", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Recommended: true for port 587", + IsLocked = false, + Key = "email.smtpUseStartTls", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 1509, + ApiKey = new Guid("a268fe56-a265-c29d-fd82-e5efc61f0505"), + Comment = "Password reset email subject line", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Subject for password reset emails", + IsLocked = false, + Key = "email.resetPassword.subject", + SortOrder = 0, + Value = "Reset your Melodee password" + }, + new + { + Id = 1600, + ApiKey = new Guid("f27eb478-3910-50ce-7a05-86aff6d0f1ca"), + Comment = "Password reset token expiry time in minutes", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "How long password reset links remain valid (default: 60 minutes)", + IsLocked = false, + Key = "security.passwordResetTokenExpiryMinutes", + SortOrder = 0, + Value = "60" + }, + new + { + Id = 1700, + ApiKey = new Guid("226cfbc6-3866-fa17-7729-23849a7b8077"), + Comment = "Enable Jellyfin API compatibility", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "When enabled, Melodee exposes Jellyfin-compatible endpoints for third-party music players", + IsLocked = false, + Key = "jellyfin.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 1701, + ApiKey = new Guid("eefa4040-71d4-b7b0-4218-52b5aa1c7408"), + Comment = "Internal route prefix for Jellyfin API", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "The internal route prefix used for Jellyfin API endpoints (default: /api/jf)", + IsLocked = false, + Key = "jellyfin.routePrefix", + SortOrder = 0, + Value = "/api/jf" + }, + new + { + Id = 1702, + ApiKey = new Guid("57d8a083-6ad7-9d6f-a31f-8b4f94e7a2a0"), + Comment = "Jellyfin token expiry time in hours", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "How long Jellyfin access tokens remain valid (default: 168 hours / 7 days)", + IsLocked = false, + Key = "jellyfin.token.expiresAfterHours", + SortOrder = 0, + Value = "168" + }, + new + { + Id = 1703, + ApiKey = new Guid("1696717a-dbe7-3278-52c1-bc43a5c7ed86"), + Comment = "Maximum active Jellyfin tokens per user", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "The maximum number of active Jellyfin tokens allowed per user (default: 10)", + IsLocked = false, + Key = "jellyfin.token.maxActivePerUser", + SortOrder = 0, + Value = "10" + }, + new + { + Id = 1704, + ApiKey = new Guid("732d29c7-1df6-4084-b126-f485463a10a4"), + Comment = "Allow legacy Emby/MediaBrowser headers", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Allow X-Emby-* and X-MediaBrowser-* headers for authentication (default: true)", + IsLocked = false, + Key = "jellyfin.token.allowLegacyHeaders", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 1705, + ApiKey = new Guid("57ef8277-a41c-a3e3-d68b-3e6c16a98728"), + Comment = "Secret pepper for Jellyfin token hashing", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Server-side secret used in token hash computation. Change this value in production for added security.", + IsLocked = false, + Key = "jellyfin.token.pepper", + SortOrder = 0, + Value = "ChangeThisPepperInProduction" + }, + new + { + Id = 1706, + ApiKey = new Guid("191427dc-3a4b-e304-fe21-9457435456d7"), + Comment = "API requests allowed per period", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Maximum number of Jellyfin API requests allowed per rate limit period (default: 200)", + IsLocked = false, + Key = "jellyfin.rateLimit.apiRequestsPerPeriod", + SortOrder = 0, + Value = "200" + }, + new + { + Id = 1707, + ApiKey = new Guid("e10e7d3e-d4e8-a507-7a8e-ff526828ddd1"), + Comment = "Rate limit period in seconds", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Duration of the rate limit period in seconds (default: 60)", + IsLocked = false, + Key = "jellyfin.rateLimit.apiPeriodSeconds", + SortOrder = 0, + Value = "60" + }, + new + { + Id = 1708, + ApiKey = new Guid("96e4d8c5-a98c-ecd1-755a-eaccd69eaa20"), + Comment = "Concurrent streams per user", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + Description = "Maximum number of concurrent audio streams allowed per user (default: 2)", + IsLocked = false, + Key = "jellyfin.rateLimit.streamConcurrentPerUser", + SortOrder = 0, + Value = "2" + }, + new + { + Id = 1709, + ApiKey = new Guid("c7b11e69-6582-e227-97ae-37435339e58e"), + Category = 9, + Comment = "Is Discogs search engine enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.discogs.enabled", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 1710, + ApiKey = new Guid("33a0d80a-8a65-e692-30a9-e3d571759efe"), + Category = 9, + Comment = "Discogs API user token for authentication.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.discogs.userToken", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1711, + ApiKey = new Guid("21837867-a824-2a66-fa7c-3583974874e4"), + Category = 9, + Comment = "Is WikiData search engine enabled.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "searchEngine.wikidata.enabled", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 1800, + ApiKey = new Guid("8ee4c50d-9a7a-a4ef-66f1-74614a24313e"), + Category = 15, + Comment = "Enable podcast support.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.enabled", + SortOrder = 0, + Value = "true" + }, + new + { + Id = 1801, + ApiKey = new Guid("c3d99d92-ab8d-bdca-ab08-3cc6ea2d2860"), + Category = 15, + Comment = "Allow HTTP (non-secure) URLs for podcast feeds.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.http.allowHttp", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 1802, + ApiKey = new Guid("93b35ab7-14d0-0814-0d66-fe040e3ae4b8"), + Category = 15, + Comment = "Timeout in seconds for HTTP requests to podcast feeds.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.http.timeoutSeconds", + SortOrder = 0, + Value = "30" + }, + new + { + Id = 1803, + ApiKey = new Guid("6b35ba44-07ac-645d-b2a3-9cadaa60ff3d"), + Category = 15, + Comment = "Maximum number of HTTP redirects to follow for podcast feeds. Podcast CDNs often use multiple analytics redirects, so 10 is recommended.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.http.maxRedirects", + SortOrder = 0, + Value = "10" + }, + new + { + Id = 1804, + ApiKey = new Guid("13168117-a286-23b5-5858-9f91485c6432"), + Category = 15, + Comment = "Maximum size in bytes for podcast feed responses.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.http.maxFeedBytes", + SortOrder = 0, + Value = "10485760" + }, + new + { + Id = 1805, + ApiKey = new Guid("1fceaf81-79eb-433c-de79-eabe193c46f8"), + Category = 15, + Comment = "Maximum number of episodes to store per podcast channel.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.refresh.maxItemsPerChannel", + SortOrder = 0, + Value = "500" + }, + new + { + Id = 1806, + ApiKey = new Guid("525bb5dc-989c-5154-0c7e-7f4b336032e3"), + Category = 15, + Comment = "Maximum concurrent podcast episode downloads (global).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.download.maxConcurrent.global", + SortOrder = 0, + Value = "2" + }, + new + { + Id = 1807, + ApiKey = new Guid("380ed177-9320-92a0-5a93-48bdcc040d35"), + Category = 15, + Comment = "Maximum concurrent podcast episode downloads per user.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.download.maxConcurrent.perUser", + SortOrder = 0, + Value = "1" + }, + new + { + Id = 1808, + ApiKey = new Guid("2d5158e7-495e-44a6-e06a-b5f1359f8ea2"), + Category = 15, + Comment = "Maximum size in bytes for podcast episode downloads.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.download.maxEnclosureBytes", + SortOrder = 0, + Value = "2147483648" + }, + new + { + Id = 1850, + ApiKey = new Guid("dc79ceff-cd68-f412-8f99-7529615cb3e8"), + Category = 14, + Comment = "Cron expression to run the podcast refresh job, set empty to disable. Default of '0 */15 * ? * *' runs every 15 minutes.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.podcastRefresh.cronExpression", + SortOrder = 0, + Value = "0 */15 * ? * *" + }, + new + { + Id = 1851, + ApiKey = new Guid("d29b11cc-d892-271a-9e2a-5eeacb795e39"), + Category = 14, + Comment = "Cron expression to run the podcast download job, set empty to disable. Default of '0 */5 * ? * *' runs every 5 minutes.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.podcastDownload.cronExpression", + SortOrder = 0, + Value = "0 */5 * ? * *" + }, + new + { + Id = 1809, + ApiKey = new Guid("908afec1-3a49-5e62-26f5-d6977ef6b00c"), + Category = 15, + Comment = "Number of days to keep downloaded episodes. 0 to disable retention.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.retention.downloadedEpisodesInDays", + SortOrder = 0, + Value = "0" + }, + new + { + Id = 1810, + ApiKey = new Guid("6f86302a-1d6d-b574-c77a-b6cfbefb5e0a"), + Category = 15, + Comment = "Threshold in minutes to consider a downloading episode as stuck.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.recovery.stuckDownloadThresholdMinutes", + SortOrder = 0, + Value = "60" + }, + new + { + Id = 1811, + ApiKey = new Guid("8d257a4b-b566-e0af-1044-9658d5ac27ea"), + Category = 15, + Comment = "Threshold in hours to consider a temporary file orphaned.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.recovery.orphanedUsageThresholdHours", + SortOrder = 0, + Value = "12" + }, + new + { + Id = 1852, + ApiKey = new Guid("3b2df55c-cd9c-a51b-2c4c-8f566bf7b6d8"), + Category = 14, + Comment = "Cron expression to run the podcast cleanup job, set empty to disable. Default of '0 0 2 * * ?' runs daily at 2 AM.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.podcastCleanup.cronExpression", + SortOrder = 0, + Value = "0 0 2 * * ?" + }, + new + { + Id = 1853, + ApiKey = new Guid("17b25fcb-6a54-291d-5927-28ade4b15a93"), + Category = 14, + Comment = "Cron expression to run the podcast recovery job, set empty to disable. Default of '0 */30 * ? * *' runs every 30 minutes.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jobs.podcastRecovery.cronExpression", + SortOrder = 0, + Value = "0 */30 * ? * *" + }, + new + { + Id = 1812, + ApiKey = new Guid("737e544b-7490-d53e-a092-3fd6e2b629b4"), + Category = 15, + Comment = "Maximum total storage in bytes for all podcasts per user. 0 for unlimited.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.quota.maxBytesPerUser", + SortOrder = 0, + Value = "5368709120" + }, + new + { + Id = 1813, + ApiKey = new Guid("153a12d4-77b4-ccc3-1584-f3685d6c9e2e"), + Category = 15, + Comment = "Keep only the last N downloaded episodes per channel. 0 to disable this policy.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.retention.keepLastNEpisodes", + SortOrder = 0, + Value = "0" + }, + new + { + Id = 1814, + ApiKey = new Guid("3da9402e-9566-c883-66e5-d232de677199"), + Category = 15, + Comment = "Delete downloaded episodes after they have been played. false to disable.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "podcast.retention.keepUnplayedOnly", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 1900, + ApiKey = new Guid("541a397c-740c-8b9d-f1ed-5f990cab92a1"), + Category = 16, + Comment = "Enable Jukebox support for server-side playback.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jukebox.enabled", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 1901, + ApiKey = new Guid("4c886427-ffc2-d277-5950-6cf4b880b7be"), + Category = 16, + Comment = "The type of backend to use for jukebox playback (e.g., 'mpv', 'mpd'). Leave empty for no backend.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "jukebox.backendType", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1910, + ApiKey = new Guid("e39d8312-cae1-ee40-266d-533077dbfdbb"), + Category = 16, + Comment = "Path to the MPV executable. Leave empty to use system PATH.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpv.path", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1911, + ApiKey = new Guid("945df58f-0546-2e6c-ccc8-210b41e719b7"), + Category = 16, + Comment = "Audio device to use for MPV playback. Leave empty for default device.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpv.audioDevice", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1912, + ApiKey = new Guid("7b99ed1d-9c95-3a2a-9aa7-aca68cda0223"), + Category = 16, + Comment = "Extra command-line arguments to pass to MPV.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpv.extraArgs", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1913, + ApiKey = new Guid("45dfa023-d926-4364-33d1-245a9623dece"), + Category = 16, + Comment = "Path for the MPV IPC socket. Leave empty for auto temp directory.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpv.socketPath", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1914, + ApiKey = new Guid("ac4199ff-57a6-9ded-7a8b-037b9df29a7f"), + Category = 16, + Comment = "Initial volume level for MPV (0.0 to 1.0). Default is 0.8.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpv.initialVolume", + SortOrder = 0, + Value = "0.8" + }, + new + { + Id = 1915, + ApiKey = new Guid("7893e826-0cc8-a0a2-12dc-5c2556212c4a"), + Category = 16, + Comment = "Enable verbose debug output for MPV.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpv.enableDebugOutput", + SortOrder = 0, + Value = "false" + }, + new + { + Id = 1920, + ApiKey = new Guid("bfcce639-8b21-dcc7-b54f-ce1d3ad074f0"), + Category = 16, + Comment = "Unique name/identifier for this MPD instance (for multi-instance support).", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpd.instanceName", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1921, + ApiKey = new Guid("275a59ef-fe5d-c2b8-28df-a7bc4a04abdb"), + Category = 16, + Comment = "Hostname or IP address of the MPD server.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpd.host", + SortOrder = 0, + Value = "localhost" + }, + new + { + Id = 1922, + ApiKey = new Guid("515116f0-99ba-30cc-4b18-d722da60cd7f"), + Category = 16, + Comment = "Port number for MPD connection.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpd.port", + SortOrder = 0, + Value = "6600" + }, + new + { + Id = 1923, + ApiKey = new Guid("dbc39d88-00c0-0710-201e-dd387d745589"), + Category = 16, + Comment = "Password for MPD authentication. Leave empty if no password.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpd.password", + SortOrder = 0, + Value = "" + }, + new + { + Id = 1924, + ApiKey = new Guid("d1d4df5f-fb55-011e-ad6a-c29db5896073"), + Category = 16, + Comment = "Timeout for MPD TCP connection and operations in milliseconds.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpd.timeoutMs", + SortOrder = 0, + Value = "10000" + }, + new + { + Id = 1925, + ApiKey = new Guid("416030fd-3e69-d30e-789f-9203464ebc86"), + Category = 16, + Comment = "Initial volume level for MPD (0.0 to 1.0). Default is 0.8.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpd.initialVolume", + SortOrder = 0, + Value = "0.8" + }, + new + { + Id = 1926, + ApiKey = new Guid("5819d3ec-0b14-1731-2179-69ab1328140b"), + Category = 16, + Comment = "Enable debug logging for MPD commands.", + CreatedAt = NodaTime.Instant.FromUnixTimeTicks(0L), + IsLocked = false, + Key = "mpd.enableDebugOutput", + SortOrder = 0, + Value = "false" + }); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Share", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDownloadable") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastVisitedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ShareId") + .HasColumnType("integer"); + + b.Property("ShareType") + .HasColumnType("integer"); + + b.Property("ShareUniqueId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("VisitCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Shares"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.ShareActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ByUserAgent") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ShareId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ShareActivities"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.SmartPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsPublic") + .HasColumnType("boolean"); + + b.Property("LastEvaluatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastResultCount") + .HasColumnType("integer"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MqlQuery") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NormalizedQuery") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("IsPublic"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("SmartPlaylists"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Song", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumId") + .HasColumnType("integer"); + + b.Property("AlternateNames") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("AmgId") + .HasColumnType("text"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("BPM") + .HasColumnType("integer"); + + b.Property("BitDepth") + .HasColumnType("integer"); + + b.Property("BitRate") + .HasColumnType("integer"); + + b.Property("CalculatedRating") + .HasColumnType("numeric"); + + b.Property("ChannelCount") + .HasColumnType("integer"); + + b.Property("Comment") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeezerId") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("DiscogsId") + .HasColumnType("text"); + + b.Property("Duration") + .HasColumnType("double precision"); + + b.Property("FileHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.PrimitiveCollection("Genres") + .HasMaxLength(2000) + .HasColumnType("text[]"); + + b.Property("ImageCount") + .HasColumnType("integer"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsVbr") + .HasColumnType("boolean"); + + b.Property("ItunesId") + .HasColumnType("text"); + + b.Property("LastFmId") + .HasColumnType("text"); + + b.Property("LastMetaDataUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPlayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Lyrics") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.PrimitiveCollection("Moods") + .HasMaxLength(2000) + .HasColumnType("text[]"); + + b.Property("MusicBrainzId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PartTitles") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("PlayedCount") + .HasColumnType("integer"); + + b.Property("ReplayGain") + .HasColumnType("double precision"); + + b.Property("ReplayPeak") + .HasColumnType("double precision"); + + b.Property("SamplingRate") + .HasColumnType("integer"); + + b.Property("SongNumber") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("SpotifyId") + .HasColumnType("text"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TitleNormalized") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TitleSort") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WikiDataId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("MusicBrainzId") + .IsUnique(); + + b.HasIndex("SpotifyId") + .IsUnique(); + + b.HasIndex("Title"); + + b.HasIndex("AlbumId", "SongNumber") + .IsUnique(); + + b.ToTable("Songs"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("EmailConfirmedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EmailNormalized") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("HasCommentRole") + .HasColumnType("boolean"); + + b.Property("HasCoverArtRole") + .HasColumnType("boolean"); + + b.Property("HasDownloadRole") + .HasColumnType("boolean"); + + b.Property("HasJukeboxRole") + .HasColumnType("boolean"); + + b.Property("HasPlaylistRole") + .HasColumnType("boolean"); + + b.Property("HasPodcastRole") + .HasColumnType("boolean"); + + b.Property("HasSettingsRole") + .HasColumnType("boolean"); + + b.Property("HasShareRole") + .HasColumnType("boolean"); + + b.Property("HasStreamRole") + .HasColumnType("boolean"); + + b.Property("HasUploadRole") + .HasColumnType("boolean"); + + b.Property("HatedGenres") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("IsEditor") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsScrobblingEnabled") + .HasColumnType("boolean"); + + b.Property("LastActivityAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFmSessionKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PasswordEncrypted") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PasswordResetToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordResetTokenExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PreferredLanguage") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("PreferredTheme") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PublicKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StarredGenres") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("TimeZoneId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UserNameNormalized") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserAlbum", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumId") + .HasColumnType("integer"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsHated") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsStarred") + .HasColumnType("boolean"); + + b.Property("LastPlayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PlayedCount") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StarredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AlbumId"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("UserId", "AlbumId") + .IsUnique(); + + b.ToTable("UserAlbums"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserArtist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("ArtistId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsHated") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsStarred") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StarredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("ArtistId"); + + b.HasIndex("UserId", "ArtistId") + .IsUnique(); + + b.ToTable("UserArtists"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserEqualizerPreset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("BandsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("UserEqualizerPresets"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserPin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PinId") + .HasColumnType("integer"); + + b.Property("PinType") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("UserId", "PinId", "PinType") + .IsUnique(); + + b.ToTable("UserPins"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserPlaybackSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("AudioQuality") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossfadeDuration") + .HasColumnType("double precision"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("EqualizerPreset") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("GaplessPlayback") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsedDevice") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ReplayGain") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("VolumeNormalization") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserPlaybackSettings"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserPodcastEpisodePlayHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ByUserAgent") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IpAddress") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsNowPlaying") + .HasColumnType("boolean"); + + b.Property("LastHeartbeatAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PodcastEpisodeId") + .HasColumnType("integer"); + + b.Property("SecondsPlayed") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PodcastEpisodeId", "PlayedAt"); + + b.HasIndex("UserId", "PodcastEpisodeId", "PlayedAt"); + + b.ToTable("UserPodcastEpisodePlayHistories"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserSocialLogin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("HostedDomain") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "Subject") + .IsUnique(); + + b.ToTable("UserSocialLogins"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("IsHated") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsStarred") + .HasColumnType("boolean"); + + b.Property("LastPlayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PlayedCount") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StarredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("SongId"); + + b.HasIndex("UserId", "SongId") + .IsUnique(); + + b.ToTable("UserSongs"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserSongPlayHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ByUserAgent") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IpAddress") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsNowPlaying") + .HasColumnType("boolean"); + + b.Property("LastHeartbeatAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecondsPlayed") + .HasColumnType("integer"); + + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PlayedAt"); + + b.HasIndex("SongId", "PlayedAt"); + + b.HasIndex("UserId", "PlayedAt"); + + b.ToTable("UserSongPlayHistories"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Album", b => + { + b.HasOne("Melodee.Common.Data.Models.Artist", "Artist") + .WithMany("Albums") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Artist", b => + { + b.HasOne("Melodee.Common.Data.Models.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.ArtistRelation", b => + { + b.HasOne("Melodee.Common.Data.Models.Artist", "Artist") + .WithMany("RelatedArtists") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.Artist", "RelatedArtist") + .WithMany() + .HasForeignKey("RelatedArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + + b.Navigation("RelatedArtist"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Bookmark", b => + { + b.HasOne("Melodee.Common.Data.Models.Song", "Song") + .WithMany("Bookmarks") + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("Bookmarks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Song"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.ChartItem", b => + { + b.HasOne("Melodee.Common.Data.Models.Chart", "Chart") + .WithMany("Items") + .HasForeignKey("ChartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.Album", "LinkedAlbum") + .WithMany() + .HasForeignKey("LinkedAlbumId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Melodee.Common.Data.Models.Artist", "LinkedArtist") + .WithMany() + .HasForeignKey("LinkedArtistId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Chart"); + + b.Navigation("LinkedAlbum"); + + b.Navigation("LinkedArtist"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Contributor", b => + { + b.HasOne("Melodee.Common.Data.Models.Album", "Album") + .WithMany("Contributors") + .HasForeignKey("AlbumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.Artist", "Artist") + .WithMany("Contributors") + .HasForeignKey("ArtistId"); + + b.HasOne("Melodee.Common.Data.Models.Song", "Song") + .WithMany("Contributors") + .HasForeignKey("SongId"); + + b.Navigation("Album"); + + b.Navigation("Artist"); + + b.Navigation("Song"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.JellyfinAccessToken", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.LibraryScanHistory", b => + { + b.HasOne("Melodee.Common.Data.Models.Library", "Library") + .WithMany("ScanHistories") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartyAuditEvent", b => + { + b.HasOne("Melodee.Common.Data.Models.PartySession", "PartySession") + .WithMany() + .HasForeignKey("PartySessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PartySession"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartyPlaybackState", b => + { + b.HasOne("Melodee.Common.Data.Models.PartyQueueItem", "CurrentQueueItem") + .WithMany() + .HasForeignKey("CurrentQueueItemApiKey") + .HasPrincipalKey("ApiKey") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Melodee.Common.Data.Models.PartySession", "PartySession") + .WithOne("PlaybackState") + .HasForeignKey("Melodee.Common.Data.Models.PartyPlaybackState", "PartySessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("CurrentQueueItem"); + + b.Navigation("PartySession"); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartyQueueItem", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "EnqueuedByUser") + .WithMany() + .HasForeignKey("EnqueuedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.PartySession", "PartySession") + .WithMany("QueueItems") + .HasForeignKey("PartySessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EnqueuedByUser"); + + b.Navigation("PartySession"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartySession", b => + { + b.HasOne("Melodee.Common.Data.Models.PartySessionEndpoint", "ActiveEndpoint") + .WithMany() + .HasForeignKey("ActiveEndpointId1"); + + b.HasOne("Melodee.Common.Data.Models.User", "OwnerUser") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ActiveEndpoint"); + + b.Navigation("OwnerUser"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartySessionEndpoint", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "OwnerUser") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OwnerUser"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartySessionParticipant", b => + { + b.HasOne("Melodee.Common.Data.Models.PartySession", "PartySession") + .WithMany("Participants") + .HasForeignKey("PartySessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PartySession"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlayQueue", b => + { + b.HasOne("Melodee.Common.Data.Models.Song", "Song") + .WithMany("PlayQues") + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("PlayQues") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Song"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Player", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("Players") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Playlist", b => + { + b.HasOne("Melodee.Common.Data.Models.PlaylistUploadedFile", "PlaylistUploadedFile") + .WithMany() + .HasForeignKey("PlaylistUploadedFileId"); + + b.HasOne("Melodee.Common.Data.Models.Song", null) + .WithMany("Playlists") + .HasForeignKey("SongId"); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("Playlists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlaylistUploadedFile"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistSong", b => + { + b.HasOne("Melodee.Common.Data.Models.Playlist", "Playlist") + .WithMany("Songs") + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.Song", "Song") + .WithMany() + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playlist"); + + b.Navigation("Song"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFile", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFileItem", b => + { + b.HasOne("Melodee.Common.Data.Models.PlaylistUploadedFile", "PlaylistUploadedFile") + .WithMany("Items") + .HasForeignKey("PlaylistUploadedFileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.Song", "Song") + .WithMany() + .HasForeignKey("SongId"); + + b.Navigation("PlaylistUploadedFile"); + + b.Navigation("Song"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PodcastEpisode", b => + { + b.HasOne("Melodee.Common.Data.Models.PodcastChannel", "PodcastChannel") + .WithMany("Episodes") + .HasForeignKey("PodcastChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PodcastChannel"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PodcastEpisodeBookmark", b => + { + b.HasOne("Melodee.Common.Data.Models.PodcastEpisode", "PodcastEpisode") + .WithMany() + .HasForeignKey("PodcastEpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PodcastEpisode"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RefreshToken", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Request", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "LastActivityUser") + .WithMany() + .HasForeignKey("LastActivityUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Melodee.Common.Data.Models.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("LastActivityUser"); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RequestComment", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Melodee.Common.Data.Models.RequestComment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Melodee.Common.Data.Models.Request", "Request") + .WithMany("Comments") + .HasForeignKey("RequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("ParentComment"); + + b.Navigation("Request"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RequestParticipant", b => + { + b.HasOne("Melodee.Common.Data.Models.Request", "Request") + .WithMany("Participants") + .HasForeignKey("RequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Request"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RequestUserState", b => + { + b.HasOne("Melodee.Common.Data.Models.Request", "Request") + .WithMany("UserStates") + .HasForeignKey("RequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Request"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Share", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("Shares") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.SmartPlaylist", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Song", b => + { + b.HasOne("Melodee.Common.Data.Models.Album", "Album") + .WithMany("Songs") + .HasForeignKey("AlbumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Album"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserAlbum", b => + { + b.HasOne("Melodee.Common.Data.Models.Album", "Album") + .WithMany("UserAlbums") + .HasForeignKey("AlbumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("UserAlbums") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Album"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserArtist", b => + { + b.HasOne("Melodee.Common.Data.Models.Artist", "Artist") + .WithMany("UserArtists") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("UserArtists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserEqualizerPreset", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserPin", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("Pins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserPlaybackSettings", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserPodcastEpisodePlayHistory", b => + { + b.HasOne("Melodee.Common.Data.Models.PodcastEpisode", "PodcastEpisode") + .WithMany() + .HasForeignKey("PodcastEpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PodcastEpisode"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserSocialLogin", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("SocialLogins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserSong", b => + { + b.HasOne("Melodee.Common.Data.Models.Song", "Song") + .WithMany("UserSongs") + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany("UserSongs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Song"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.UserSongPlayHistory", b => + { + b.HasOne("Melodee.Common.Data.Models.Song", "Song") + .WithMany() + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Song"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Album", b => + { + b.Navigation("Contributors"); + + b.Navigation("Songs"); + + b.Navigation("UserAlbums"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Artist", b => + { + b.Navigation("Albums"); + + b.Navigation("Contributors"); + + b.Navigation("RelatedArtists"); + + b.Navigation("UserArtists"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Chart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Library", b => + { + b.Navigation("ScanHistories"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PartySession", b => + { + b.Navigation("Participants"); + + b.Navigation("PlaybackState"); + + b.Navigation("QueueItems"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Playlist", b => + { + b.Navigation("Songs"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFile", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PodcastChannel", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Request", b => + { + b.Navigation("Comments"); + + b.Navigation("Participants"); + + b.Navigation("UserStates"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.RequestComment", b => + { + b.Navigation("Replies"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.Song", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("Contributors"); + + b.Navigation("PlayQues"); + + b.Navigation("Playlists"); + + b.Navigation("UserSongs"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.User", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("Pins"); + + b.Navigation("PlayQues"); + + b.Navigation("Players"); + + b.Navigation("Playlists"); + + b.Navigation("RefreshTokens"); + + b.Navigation("Shares"); + + b.Navigation("SocialLogins"); + + b.Navigation("UserAlbums"); + + b.Navigation("UserArtists"); + + b.Navigation("UserSongs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Melodee.Common/Migrations/20260117043252_AddPlaylistImportModels.cs b/src/Melodee.Common/Migrations/20260117043252_AddPlaylistImportModels.cs new file mode 100644 index 00000000..7c6c3bde --- /dev/null +++ b/src/Melodee.Common/Migrations/20260117043252_AddPlaylistImportModels.cs @@ -0,0 +1,262 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Melodee.Common.Migrations +{ + /// + public partial class AddPlaylistImportModels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PlaylistUploadedFileId", + table: "Playlists", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "SourceType", + table: "Playlists", + type: "smallint", + nullable: false, + defaultValue: (short)0); + + migrationBuilder.CreateTable( + name: "PlaylistUploadedFiles", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "integer", nullable: false), + OriginalFileName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + ContentType = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Length = table.Column(type: "bigint", nullable: false), + FileData = table.Column(type: "bytea", nullable: true), + IsLocked = table.Column(type: "boolean", nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + ApiKey = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + LastUpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + Tags = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + Notes = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + Description = table.Column(type: "character varying(62000)", maxLength: 62000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PlaylistUploadedFiles", x => x.Id); + table.ForeignKey( + name: "FK_PlaylistUploadedFiles_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PlaylistUploadedFileItems", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PlaylistUploadedFileId = table.Column(type: "integer", nullable: false), + SongId = table.Column(type: "integer", nullable: true), + SortOrder = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "smallint", nullable: false), + RawReference = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), + NormalizedReference = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), + HintsJson = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + LastAttemptUtc = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PlaylistUploadedFileItems", x => x.Id); + table.ForeignKey( + name: "FK_PlaylistUploadedFileItems_PlaylistUploadedFiles_PlaylistUpl~", + column: x => x.PlaylistUploadedFileId, + principalTable: "PlaylistUploadedFiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PlaylistUploadedFileItems_Songs_SongId", + column: x => x.SongId, + principalTable: "Songs", + principalColumn: "Id"); + }); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 1, + column: "Path", + value: "/app/inbound/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 2, + column: "Path", + value: "/app/staging/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 3, + column: "Path", + value: "/app/storage/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 4, + column: "Path", + value: "/app/user-images/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 5, + column: "Path", + value: "/app/playlists/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 6, + column: "Path", + value: "/app/templates/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 7, + column: "Path", + value: "/app/podcasts/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 8, + column: "Path", + value: "/app/themes/"); + + migrationBuilder.CreateIndex( + name: "IX_Playlists_PlaylistUploadedFileId", + table: "Playlists", + column: "PlaylistUploadedFileId"); + + migrationBuilder.CreateIndex( + name: "IX_PlaylistUploadedFileItems_PlaylistUploadedFileId_SortOrder", + table: "PlaylistUploadedFileItems", + columns: new[] { "PlaylistUploadedFileId", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_PlaylistUploadedFileItems_SongId", + table: "PlaylistUploadedFileItems", + column: "SongId"); + + migrationBuilder.CreateIndex( + name: "IX_PlaylistUploadedFiles_ApiKey", + table: "PlaylistUploadedFiles", + column: "ApiKey", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PlaylistUploadedFiles_UserId", + table: "PlaylistUploadedFiles", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Playlists_PlaylistUploadedFiles_PlaylistUploadedFileId", + table: "Playlists", + column: "PlaylistUploadedFileId", + principalTable: "PlaylistUploadedFiles", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Playlists_PlaylistUploadedFiles_PlaylistUploadedFileId", + table: "Playlists"); + + migrationBuilder.DropTable( + name: "PlaylistUploadedFileItems"); + + migrationBuilder.DropTable( + name: "PlaylistUploadedFiles"); + + migrationBuilder.DropIndex( + name: "IX_Playlists_PlaylistUploadedFileId", + table: "Playlists"); + + migrationBuilder.DropColumn( + name: "PlaylistUploadedFileId", + table: "Playlists"); + + migrationBuilder.DropColumn( + name: "SourceType", + table: "Playlists"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 1, + column: "Path", + value: "/storage/inbound/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 2, + column: "Path", + value: "/storage/staging/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 3, + column: "Path", + value: "/storage/library/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 4, + column: "Path", + value: "/storage/images/users/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 5, + column: "Path", + value: "/storage/playlists/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 6, + column: "Path", + value: "/storage/templates/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 7, + column: "Path", + value: "/storage/podcasts/"); + + migrationBuilder.UpdateData( + table: "Libraries", + keyColumn: "Id", + keyValue: 8, + column: "Path", + value: "/storage/themes/"); + } + } +} diff --git a/src/Melodee.Common/Migrations/MelodeeDbContextModelSnapshot.cs b/src/Melodee.Common/Migrations/MelodeeDbContextModelSnapshot.cs index bcc39180..df8079dc 100644 --- a/src/Melodee.Common/Migrations/MelodeeDbContextModelSnapshot.cs +++ b/src/Melodee.Common/Migrations/MelodeeDbContextModelSnapshot.cs @@ -908,7 +908,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Description = "Files in this directory are scanned and Album information is gathered via processing.", IsLocked = false, Name = "Inbound", - Path = "/storage/inbound/", + Path = "/app/inbound/", SortOrder = 0, Type = 1 }, @@ -920,7 +920,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Description = "The staging directory to place processed files into (Inbound -> Staging -> Library).", IsLocked = false, Name = "Staging", - Path = "/storage/staging/", + Path = "/app/staging/", SortOrder = 0, Type = 2 }, @@ -932,7 +932,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Description = "The library directory to place processed, reviewed and ready to use music files into.", IsLocked = false, Name = "Storage", - Path = "/storage/library/", + Path = "/app/storage/", SortOrder = 0, Type = 3 }, @@ -944,7 +944,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Description = "Library where user images are stored.", IsLocked = false, Name = "User Images", - Path = "/storage/images/users/", + Path = "/app/user-images/", SortOrder = 0, Type = 4 }, @@ -956,7 +956,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Description = "Library where playlist data is stored.", IsLocked = false, Name = "Playlist Data", - Path = "/storage/playlists/", + Path = "/app/playlists/", SortOrder = 0, Type = 5 }, @@ -968,7 +968,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Description = "Library where templates are stored, organized by language code.", IsLocked = false, Name = "Templates", - Path = "/storage/templates/", + Path = "/app/templates/", SortOrder = 0, Type = 7 }, @@ -980,7 +980,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Description = "Library where podcast media files are stored.", IsLocked = false, Name = "Podcasts", - Path = "/storage/podcasts/", + Path = "/app/podcasts/", SortOrder = 0, Type = 8 }, @@ -992,7 +992,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) Description = "Library where custom theme packs are stored.", IsLocked = false, Name = "Themes", - Path = "/storage/themes/", + Path = "/app/themes/", SortOrder = 0, Type = 9 }); @@ -1639,6 +1639,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(4000) .HasColumnType("character varying(4000)"); + b.Property("PlaylistUploadedFileId") + .HasColumnType("integer"); + b.Property("SongCount") .HasColumnType("smallint"); @@ -1648,6 +1651,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("SortOrder") .HasColumnType("integer"); + b.Property("SourceType") + .HasColumnType("smallint"); + b.Property("Tags") .HasMaxLength(2000) .HasColumnType("character varying(2000)"); @@ -1660,6 +1666,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ApiKey") .IsUnique(); + b.HasIndex("PlaylistUploadedFileId"); + b.HasIndex("SongId"); b.HasIndex("UserId", "Name") @@ -1692,6 +1700,116 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("PlaylistSong"); }); + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(62000) + .HasColumnType("character varying(62000)"); + + b.Property("FileData") + .HasColumnType("bytea"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApiKey") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("PlaylistUploadedFiles"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFileItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HintsJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("LastAttemptUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedReference") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("PlaylistUploadedFileId") + .HasColumnType("integer"); + + b.Property("RawReference") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("SongId"); + + b.HasIndex("PlaylistUploadedFileId", "SortOrder"); + + b.ToTable("PlaylistUploadedFileItems"); + }); + modelBuilder.Entity("Melodee.Common.Data.Models.PodcastChannel", b => { b.Property("Id") @@ -5655,6 +5773,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Melodee.Common.Data.Models.Playlist", b => { + b.HasOne("Melodee.Common.Data.Models.PlaylistUploadedFile", "PlaylistUploadedFile") + .WithMany() + .HasForeignKey("PlaylistUploadedFileId"); + b.HasOne("Melodee.Common.Data.Models.Song", null) .WithMany("Playlists") .HasForeignKey("SongId"); @@ -5665,6 +5787,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("PlaylistUploadedFile"); + b.Navigation("User"); }); @@ -5687,6 +5811,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Song"); }); + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFile", b => + { + b.HasOne("Melodee.Common.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFileItem", b => + { + b.HasOne("Melodee.Common.Data.Models.PlaylistUploadedFile", "PlaylistUploadedFile") + .WithMany("Items") + .HasForeignKey("PlaylistUploadedFileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Melodee.Common.Data.Models.Song", "Song") + .WithMany() + .HasForeignKey("SongId"); + + b.Navigation("PlaylistUploadedFile"); + + b.Navigation("Song"); + }); + modelBuilder.Entity("Melodee.Common.Data.Models.PodcastEpisode", b => { b.HasOne("Melodee.Common.Data.Models.PodcastChannel", "PodcastChannel") @@ -6033,6 +6185,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Songs"); }); + modelBuilder.Entity("Melodee.Common.Data.Models.PlaylistUploadedFile", b => + { + b.Navigation("Items"); + }); + modelBuilder.Entity("Melodee.Common.Data.Models.PodcastChannel", b => { b.Navigation("Episodes"); diff --git a/src/Melodee.Common/Models/PlaylistImportResult.cs b/src/Melodee.Common/Models/PlaylistImportResult.cs new file mode 100644 index 00000000..c25c5efa --- /dev/null +++ b/src/Melodee.Common/Models/PlaylistImportResult.cs @@ -0,0 +1,11 @@ +namespace Melodee.Common.Models; + +public sealed record PlaylistImportResult +{ + public int PlaylistId { get; init; } + public Guid PlaylistApiKey { get; init; } + public string PlaylistName { get; init; } = string.Empty; + public int TotalEntries { get; init; } + public int MatchedEntries { get; init; } + public int MissingEntries { get; init; } +} diff --git a/src/Melodee.Common/Services/Parsing/M3UParser.cs b/src/Melodee.Common/Services/Parsing/M3UParser.cs new file mode 100644 index 00000000..638de2ce --- /dev/null +++ b/src/Melodee.Common/Services/Parsing/M3UParser.cs @@ -0,0 +1,205 @@ +using System.Text; +using System.Web; +using Serilog; + +namespace Melodee.Common.Services.Parsing; + +/// +/// Parser for M3U and M3U8 playlist files +/// +public sealed class M3UParser +{ + private readonly ILogger _logger; + + public M3UParser(ILogger logger) + { + _logger = logger; + } + + /// + /// Parse an M3U/M3U8 file and return a list of normalized entry references + /// + public async Task ParseAsync(Stream fileStream, string fileName, CancellationToken cancellationToken = default) + { + var isM3U8 = fileName.EndsWith(".m3u8", StringComparison.OrdinalIgnoreCase); + var encoding = isM3U8 ? Encoding.UTF8 : DetectEncoding(fileStream); + + fileStream.Position = 0; + + var entries = new List(); + var lineNumber = 0; + + using var reader = new StreamReader(fileStream, encoding, detectEncodingFromByteOrderMarks: true, leaveOpen: true); + + string? line; + while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null && !cancellationToken.IsCancellationRequested) + { + lineNumber++; + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + line = line.Trim(); + + // Skip comments (including #EXTM3U and #EXTINF) + if (line.StartsWith('#')) + { + continue; + } + + try + { + var entry = NormalizeEntry(line, lineNumber); + entries.Add(entry); + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to parse line {LineNumber} in {FileName}: {Line}", lineNumber, fileName, line); + // Add as missing entry with raw reference + entries.Add(new M3UEntry + { + RawReference = line, + NormalizedReference = line, + SortOrder = lineNumber, + ParseError = ex.Message + }); + } + } + + return new M3UParseResult + { + Entries = entries, + TotalLines = lineNumber, + FileName = fileName, + Encoding = encoding.WebName + }; + } + + private static Encoding DetectEncoding(Stream stream) + { + var buffer = new byte[4]; + var bytesRead = stream.Read(buffer, 0, 4); + stream.Position = 0; + + if (bytesRead >= 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF) + { + return Encoding.UTF8; + } + + if (bytesRead >= 2 && buffer[0] == 0xFF && buffer[1] == 0xFE) + { + return Encoding.Unicode; + } + + if (bytesRead >= 2 && buffer[0] == 0xFE && buffer[1] == 0xFF) + { + return Encoding.BigEndianUnicode; + } + + // Default to UTF-8 for M3U files + return Encoding.UTF8; + } + + private static M3UEntry NormalizeEntry(string rawLine, int sortOrder) + { + var normalized = rawLine; + + // Remove surrounding quotes if present + if ((normalized.StartsWith('"') && normalized.EndsWith('"')) || + (normalized.StartsWith('\'') && normalized.EndsWith('\''))) + { + normalized = normalized[1..^1]; + } + + // URL decode (handle %xx sequences) + normalized = SafeUrlDecode(normalized); + + // Normalize path separators (convert backslashes to forward slashes) + normalized = normalized.Replace('\\', '/'); + + // Extract path hints (artist folder, album folder, filename) + var hints = ExtractPathHints(normalized); + + return new M3UEntry + { + RawReference = rawLine, + NormalizedReference = normalized, + SortOrder = sortOrder, + FileName = hints.FileName, + ArtistFolder = hints.ArtistFolder, + AlbumFolder = hints.AlbumFolder + }; + } + + private static string SafeUrlDecode(string input) + { + try + { + // Only decode if it contains % characters + if (!input.Contains('%')) + { + return input; + } + + // Use HttpUtility.UrlDecode which handles literal % safely + var decoded = HttpUtility.UrlDecode(input); + return decoded ?? input; + } + catch + { + // If decoding fails, return original + return input; + } + } + + private static (string? FileName, string? ArtistFolder, string? AlbumFolder) ExtractPathHints(string normalizedPath) + { + try + { + // Remove any protocol/scheme (http://, file://, etc.) + if (normalizedPath.Contains("://")) + { + return (normalizedPath, null, null); + } + + // Split path into segments + var segments = normalizedPath.Split('/', StringSplitOptions.RemoveEmptyEntries); + + if (segments.Length == 0) + { + return (normalizedPath, null, null); + } + + var fileName = segments[^1]; + string? albumFolder = segments.Length >= 2 ? segments[^2] : null; + string? artistFolder = segments.Length >= 3 ? segments[^3] : null; + + return (fileName, artistFolder, albumFolder); + } + catch + { + return (normalizedPath, null, null); + } + } +} + +public sealed class M3UParseResult +{ + public required List Entries { get; init; } + public required int TotalLines { get; init; } + public required string FileName { get; init; } + public required string Encoding { get; init; } +} + +public sealed class M3UEntry +{ + public required string RawReference { get; init; } + public required string NormalizedReference { get; init; } + public required int SortOrder { get; init; } + public string? FileName { get; init; } + public string? ArtistFolder { get; init; } + public string? AlbumFolder { get; init; } + public string? ParseError { get; init; } +} diff --git a/src/Melodee.Common/Services/PlaylistService.cs b/src/Melodee.Common/Services/PlaylistService.cs index 2ab6f215..347cbcc7 100644 --- a/src/Melodee.Common/Services/PlaylistService.cs +++ b/src/Melodee.Common/Services/PlaylistService.cs @@ -1292,4 +1292,182 @@ public async Task> DeleteByApiKeyAsync( Data = returnPrefixedApiKey ? newPlaylist.ToApiKey() : newPlaylist.ApiKey.ToString() }; } + + /// + /// Import an M3U/M3U8 playlist file into the system + /// + public async Task> ImportPlaylistAsync( + int userId, + Stream fileStream, + string fileName, + CancellationToken cancellationToken = default) + { + await using var scopedContext = await ContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + + try + { + var now = SystemClock.Instance.GetCurrentInstant(); + + // Read file bytes for storage + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); + var fileBytes = memoryStream.ToArray(); + + // Parse the M3U file + memoryStream.Position = 0; + var parser = new Parsing.M3UParser(Logger); + var parseResult = await parser.ParseAsync(memoryStream, fileName, cancellationToken).ConfigureAwait(false); + + if (parseResult.Entries.Count == 0) + { + return new OperationResult + { + Data = new PlaylistImportResult + { + PlaylistId = 0, + PlaylistApiKey = Guid.Empty, + TotalEntries = 0, + MatchedEntries = 0, + MissingEntries = 0, + PlaylistName = string.Empty + }, + Type = OperationResponseType.Error, + Errors = new[] { new Exception("Playlist file contains no valid entries") } + }; + } + + // Create uploaded file record + var uploadedFile = new PlaylistUploadedFile + { + UserId = userId, + OriginalFileName = fileName, + ContentType = fileName.EndsWith(".m3u8", StringComparison.OrdinalIgnoreCase) + ? "audio/x-mpegurl; charset=utf-8" + : "audio/x-mpegurl", + Length = fileBytes.Length, + FileData = fileBytes, + CreatedAt = now + }; + + await scopedContext.PlaylistUploadedFiles.AddAsync(uploadedFile, cancellationToken).ConfigureAwait(false); + await scopedContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + // Get library path for matching (use first storage library) + var librariesResult = await libraryService.GetStorageLibrariesAsync(cancellationToken).ConfigureAwait(false); + var libraryPath = librariesResult.Data?.FirstOrDefault()?.Path; + + // Create song matching service + var songMatcher = new SongMatchingService(Logger, CacheManager, ContextFactory); + + // Match songs and track results + var matchedSongs = new List(); + var missingItems = new List(); + var matchedCount = 0; + + foreach (var entry in parseResult.Entries) + { + var matchResult = await songMatcher.MatchEntryAsync(entry, libraryPath, cancellationToken).ConfigureAwait(false); + + var item = new PlaylistUploadedFileItem + { + PlaylistUploadedFileId = uploadedFile.Id, + SongId = matchResult.Song?.Id, + SortOrder = entry.SortOrder, + Status = matchResult.Song != null ? Enums.PlaylistItemStatus.Resolved : Enums.PlaylistItemStatus.Missing, + RawReference = entry.RawReference, + NormalizedReference = entry.NormalizedReference, + HintsJson = serializer.Serialize(new + { + FileName = entry.FileName, + ArtistFolder = entry.ArtistFolder, + AlbumFolder = entry.AlbumFolder, + MatchStrategy = matchResult.MatchStrategy.ToString(), + Confidence = matchResult.Confidence + }), + LastAttemptUtc = now + }; + + if (matchResult.Song != null) + { + matchedSongs.Add(matchResult.Song); + matchedCount++; + } + else + { + missingItems.Add(item); + } + + await scopedContext.PlaylistUploadedFileItems.AddAsync(item, cancellationToken).ConfigureAwait(false); + } + + // Create the playlist + var playlistName = Path.GetFileNameWithoutExtension(fileName); + var playlist = new Playlist + { + CreatedAt = now, + Name = playlistName, + Comment = $"Imported from {fileName}", + IsPublic = false, + UserId = userId, + SourceType = Enums.PlaylistSourceType.M3UImport, + PlaylistUploadedFileId = uploadedFile.Id, + SongCount = SafeParser.ToNumber(matchedSongs.Count), + Duration = matchedSongs.Sum(x => x.Duration), + Songs = matchedSongs.Select((song, index) => new PlaylistSong + { + SongId = song.Id, + SongApiKey = song.ApiKey, + PlaylistOrder = index + }).ToArray() + }; + + await scopedContext.Playlists.AddAsync(playlist, cancellationToken).ConfigureAwait(false); + await scopedContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + Logger.Information( + "Playlist imported from [{FileName}] for user [{UserId}]. Matched: {Matched}/{Total} songs", + fileName, userId, matchedCount, parseResult.Entries.Count); + + return new OperationResult + { + Data = new PlaylistImportResult + { + PlaylistId = playlist.Id, + PlaylistApiKey = playlist.ApiKey, + TotalEntries = parseResult.Entries.Count, + MatchedEntries = matchedCount, + MissingEntries = parseResult.Entries.Count - matchedCount, + PlaylistName = playlist.Name + } + }; + } + catch (Exception ex) + { + Logger.Error(ex, "Error importing playlist from file [{FileName}]", fileName); + return new OperationResult + { + Data = new PlaylistImportResult + { + PlaylistId = 0, + PlaylistApiKey = Guid.Empty, + TotalEntries = 0, + MatchedEntries = 0, + MissingEntries = 0, + PlaylistName = string.Empty + }, + Type = OperationResponseType.Error, + Errors = new[] { ex } + }; + } + } +} + +public sealed class PlaylistImportResult +{ + public int PlaylistId { get; init; } + public Guid PlaylistApiKey { get; init; } + public int TotalEntries { get; init; } + public int MatchedEntries { get; init; } + public int MissingEntries { get; init; } + public required string PlaylistName { get; init; } } diff --git a/src/Melodee.Common/Services/SongMatchingService.cs b/src/Melodee.Common/Services/SongMatchingService.cs new file mode 100644 index 00000000..9c515aa7 --- /dev/null +++ b/src/Melodee.Common/Services/SongMatchingService.cs @@ -0,0 +1,335 @@ +using Melodee.Common.Data; +using Melodee.Common.Data.Models; +using Melodee.Common.Services.Caching; +using Melodee.Common.Services.Parsing; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace Melodee.Common.Services; + +/// +/// Service for matching M3U playlist entries to songs in the library +/// +public sealed class SongMatchingService( + ILogger logger, + ICacheManager cacheManager, + IDbContextFactory contextFactory) + : ServiceBase(logger, cacheManager, contextFactory) +{ + /// + /// Attempt to match an M3U entry to a song in the library + /// + public async Task MatchEntryAsync( + M3UEntry entry, + string? libraryPath = null, + CancellationToken cancellationToken = default) + { + await using var scopedContext = await ContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + + // Strategy 1: Exact path match under library root + if (!string.IsNullOrEmpty(libraryPath)) + { + var exactMatch = await TryExactPathMatchAsync(scopedContext, entry, libraryPath, cancellationToken).ConfigureAwait(false); + if (exactMatch != null) + { + return new SongMatchResult + { + Song = exactMatch, + MatchStrategy = MatchStrategy.ExactPath, + Confidence = 1.0m + }; + } + } + + // Strategy 2: Filename with directory hints + if (!string.IsNullOrEmpty(entry.FileName)) + { + var filenameMatch = await TryFilenameMatchAsync(scopedContext, entry, cancellationToken).ConfigureAwait(false); + if (filenameMatch.HasValue) + { + return new SongMatchResult + { + Song = filenameMatch.Value.Song, + MatchStrategy = MatchStrategy.FilenameWithHints, + Confidence = filenameMatch.Value.Confidence + }; + } + } + + // Strategy 3: Metadata match (title + artist + album) + var metadataMatch = await TryMetadataMatchAsync(scopedContext, entry, cancellationToken).ConfigureAwait(false); + if (metadataMatch.HasValue) + { + return new SongMatchResult + { + Song = metadataMatch.Value.Song, + MatchStrategy = MatchStrategy.Metadata, + Confidence = metadataMatch.Value.Confidence + }; + } + + // No match found + return new SongMatchResult + { + Song = null, + MatchStrategy = MatchStrategy.None, + Confidence = 0m + }; + } + + private async Task TryExactPathMatchAsync( + MelodeeDbContext context, + M3UEntry entry, + string libraryPath, + CancellationToken cancellationToken) + { + try + { + // Normalize library path + var normalizedLibraryPath = libraryPath.Replace('\\', '/').TrimEnd('/'); + var normalizedReference = entry.NormalizedReference; + + // Remove drive letters and leading slashes for comparison + if (normalizedReference.Length > 2 && normalizedReference[1] == ':') + { + // Windows absolute path like "D:/Music/..." + normalizedReference = normalizedReference[2..].TrimStart('/'); + } + else if (normalizedReference.StartsWith('/')) + { + // Unix absolute path like "/music/..." + normalizedReference = normalizedReference.TrimStart('/'); + } + + // Try to find song with matching file path + var candidatePaths = new[] + { + normalizedReference, + Path.Combine(normalizedLibraryPath, normalizedReference).Replace('\\', '/'), + normalizedReference.Split('/').Last() // Just filename + }; + + var songsMatchingPaths = candidatePaths + .Select(candidatePath => context.Songs + .Include(s => s.Album) + .ThenInclude(a => a.Artist) + .FirstOrDefaultAsync(s => + s.FileName.Replace('\\', '/').EndsWith("/" + candidatePath) || + s.FileName.Replace('\\', '/') == candidatePath, + cancellationToken)) + .ToList(); + + foreach (var songTask in songsMatchingPaths) + { + var song = await songTask.ConfigureAwait(false); + if (song != null) + { + return song; + } + } + } + catch (Exception ex) + { + Logger.Warning(ex, "Error during exact path matching for entry: {Entry}", entry.NormalizedReference); + } + + return null; + } + + private async Task<(Song Song, decimal Confidence)?> TryFilenameMatchAsync( + MelodeeDbContext context, + M3UEntry entry, + CancellationToken cancellationToken) + { + try + { + if (string.IsNullOrEmpty(entry.FileName)) + { + return null; + } + + var query = context.Songs + .Include(s => s.Album) + .ThenInclude(a => a.Artist) + .AsQueryable(); + + // Match by filename (song file path ends with the filename) + query = query.Where(s => s.FileName.Contains(entry.FileName)); + + // Apply album folder hint if available + if (!string.IsNullOrEmpty(entry.AlbumFolder)) + { + var albumHint = entry.AlbumFolder; + query = query.Where(s => s.Album.Name.Contains(albumHint) || s.Album.NameNormalized.Contains(albumHint)); + } + + // Apply artist folder hint if available + if (!string.IsNullOrEmpty(entry.ArtistFolder)) + { + var artistHint = entry.ArtistFolder; + query = query.Where(s => s.Album.Artist.Name.Contains(artistHint) || s.Album.Artist.NameNormalized.Contains(artistHint)); + } + + var candidates = await query.Take(10).ToListAsync(cancellationToken).ConfigureAwait(false); + + if (candidates.Count == 0) + { + return null; + } + + // If multiple matches, score them + if (candidates.Count == 1) + { + return (candidates[0], 0.8m); + } + + // Score based on how well hints match + var scored = candidates.Select(song => new + { + Song = song, + Score = CalculateFileMatchScore(song, entry) + }).OrderByDescending(x => x.Score).First(); + + return scored.Score > 0 ? (scored.Song, scored.Score) : null; + } + catch (Exception ex) + { + Logger.Warning(ex, "Error during filename matching for entry: {Entry}", entry.FileName); + } + + return null; + } + + private async Task<(Song Song, decimal Confidence)?> TryMetadataMatchAsync( + MelodeeDbContext context, + M3UEntry entry, + CancellationToken cancellationToken) + { + try + { + // Extract potential song title from filename + if (string.IsNullOrEmpty(entry.FileName)) + { + return null; + } + + var potentialTitle = Path.GetFileNameWithoutExtension(entry.FileName); + + // Remove common track number prefixes (01 - Title, 01. Title, etc.) + potentialTitle = System.Text.RegularExpressions.Regex.Replace(potentialTitle, @"^\d+[\s\.\-_]+", ""); + + var query = context.Songs + .Include(s => s.Album) + .ThenInclude(a => a.Artist) + .Where(s => s.TitleNormalized.Contains(potentialTitle) || s.Title.Contains(potentialTitle)); + + // Apply album hint if available + if (!string.IsNullOrEmpty(entry.AlbumFolder)) + { + query = query.Where(s => s.Album.NameNormalized.Contains(entry.AlbumFolder)); + } + + // Apply artist hint if available + if (!string.IsNullOrEmpty(entry.ArtistFolder)) + { + query = query.Where(s => s.Album.Artist.NameNormalized.Contains(entry.ArtistFolder)); + } + + var candidates = await query.Take(5).ToListAsync(cancellationToken).ConfigureAwait(false); + + if (candidates.Count == 0) + { + return null; + } + + if (candidates.Count == 1) + { + return (candidates[0], 0.6m); + } + + // Score candidates based on metadata similarity + var scored = candidates.Select(song => new + { + Song = song, + Score = CalculateMetadataMatchScore(song, entry, potentialTitle) + }).OrderByDescending(x => x.Score).First(); + + return scored.Score > 0.3m ? (scored.Song, scored.Score) : null; + } + catch (Exception ex) + { + Logger.Warning(ex, "Error during metadata matching for entry: {Entry}", entry.FileName); + } + + return null; + } + + private static decimal CalculateFileMatchScore(Song song, M3UEntry entry) + { + decimal score = 0.5m; // Base score for filename match + + // Bonus for album folder match + if (!string.IsNullOrEmpty(entry.AlbumFolder) && + (song.Album.Name.Contains(entry.AlbumFolder, StringComparison.OrdinalIgnoreCase) || + song.Album.NameNormalized.Contains(entry.AlbumFolder, StringComparison.OrdinalIgnoreCase))) + { + score += 0.2m; + } + + // Bonus for artist folder match + if (!string.IsNullOrEmpty(entry.ArtistFolder) && + (song.Album.Artist.Name.Contains(entry.ArtistFolder, StringComparison.OrdinalIgnoreCase) || + song.Album.Artist.NameNormalized.Contains(entry.ArtistFolder, StringComparison.OrdinalIgnoreCase))) + { + score += 0.2m; + } + + return Math.Min(score, 0.9m); // Cap at 0.9 (not perfect match like exact path) + } + + private static decimal CalculateMetadataMatchScore(Song song, M3UEntry entry, string potentialTitle) + { + decimal score = 0.3m; // Base score for metadata match + + // Check title similarity + if (song.TitleNormalized.Equals(potentialTitle, StringComparison.OrdinalIgnoreCase)) + { + score += 0.3m; + } + else if (song.TitleNormalized.Contains(potentialTitle, StringComparison.OrdinalIgnoreCase)) + { + score += 0.15m; + } + + // Bonus for album match + if (!string.IsNullOrEmpty(entry.AlbumFolder) && + song.Album.NameNormalized.Equals(entry.AlbumFolder, StringComparison.OrdinalIgnoreCase)) + { + score += 0.2m; + } + + // Bonus for artist match + if (!string.IsNullOrEmpty(entry.ArtistFolder) && + song.Album.Artist.NameNormalized.Equals(entry.ArtistFolder, StringComparison.OrdinalIgnoreCase)) + { + score += 0.2m; + } + + return Math.Min(score, 0.7m); // Cap metadata matches lower than file matches + } +} + +public enum MatchStrategy +{ + None = 0, + ExactPath = 1, + FilenameWithHints = 2, + Metadata = 3 +} + +public sealed class SongMatchResult +{ + public Song? Song { get; init; } + public MatchStrategy MatchStrategy { get; init; } + public decimal Confidence { get; init; } +} diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.Build.Locator.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.Build.Locator.dll" new file mode 100755 index 00000000..13b1021e Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.Build.Locator.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe" new file mode 100755 index 00000000..00dd99f7 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe.config" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe.config" new file mode 100755 index 00000000..f52998b2 --- /dev/null +++ "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe.config" @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.IO.Redist.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.IO.Redist.dll" new file mode 100755 index 00000000..88e63d82 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Microsoft.IO.Redist.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Newtonsoft.Json.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Newtonsoft.Json.dll" new file mode 100755 index 00000000..1d035d63 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/Newtonsoft.Json.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Buffers.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Buffers.dll" new file mode 100755 index 00000000..f2d83c51 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Buffers.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Collections.Immutable.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Collections.Immutable.dll" new file mode 100755 index 00000000..7594b2e1 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Collections.Immutable.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.CommandLine.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.CommandLine.dll" new file mode 100755 index 00000000..d0bbad5d Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.CommandLine.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Memory.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Memory.dll" new file mode 100755 index 00000000..46171997 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Memory.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Numerics.Vectors.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Numerics.Vectors.dll" new file mode 100755 index 00000000..08659724 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Numerics.Vectors.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Runtime.CompilerServices.Unsafe.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Runtime.CompilerServices.Unsafe.dll" new file mode 100755 index 00000000..c5ba4e40 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Runtime.CompilerServices.Unsafe.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Threading.Tasks.Extensions.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Threading.Tasks.Extensions.dll" new file mode 100755 index 00000000..eeec9285 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/System.Threading.Tasks.Extensions.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/cs/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/cs/System.CommandLine.resources.dll" new file mode 100755 index 00000000..0be3757c Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/cs/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/de/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/de/System.CommandLine.resources.dll" new file mode 100755 index 00000000..bfed293e Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/de/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/es/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/es/System.CommandLine.resources.dll" new file mode 100755 index 00000000..5e1c416f Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/es/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/fr/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/fr/System.CommandLine.resources.dll" new file mode 100755 index 00000000..2916bdf2 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/fr/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/it/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/it/System.CommandLine.resources.dll" new file mode 100755 index 00000000..1a55c94f Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/it/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/ja/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/ja/System.CommandLine.resources.dll" new file mode 100755 index 00000000..c1be1539 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/ja/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/ko/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/ko/System.CommandLine.resources.dll" new file mode 100755 index 00000000..bfcbbc61 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/ko/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/pl/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/pl/System.CommandLine.resources.dll" new file mode 100755 index 00000000..b9efaec6 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/pl/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/pt-BR/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/pt-BR/System.CommandLine.resources.dll" new file mode 100755 index 00000000..69612cbc Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/pt-BR/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/ru/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/ru/System.CommandLine.resources.dll" new file mode 100755 index 00000000..042aaf8e Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/ru/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/tr/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/tr/System.CommandLine.resources.dll" new file mode 100755 index 00000000..629b98b4 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/tr/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/zh-Hans/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/zh-Hans/System.CommandLine.resources.dll" new file mode 100755 index 00000000..ff8dacbf Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/zh-Hans/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/zh-Hant/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/zh-Hant/System.CommandLine.resources.dll" new file mode 100755 index 00000000..9b9870a0 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-net472/zh-Hant/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.Build.Locator.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.Build.Locator.dll" new file mode 100755 index 00000000..cafcf213 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.Build.Locator.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.deps.json" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.deps.json" new file mode 100755 index 00000000..ed7fe7a5 --- /dev/null +++ "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.deps.json" @@ -0,0 +1,260 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/4.14.0-3.25262.10": { + "dependencies": { + "Microsoft.Build.Locator": "1.6.10", + "Microsoft.CodeAnalysis.NetAnalyzers": "8.0.0-preview.23468.1", + "Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers": "3.3.4-beta1.22504.1", + "Microsoft.DotNet.XliffTasks": "9.0.0-beta.25255.5", + "Microsoft.VisualStudio.Threading.Analyzers": "17.13.2", + "Newtonsoft.Json": "13.0.3", + "Roslyn.Diagnostics.Analyzers": "3.11.0-beta1.24081.1", + "System.Collections.Immutable": "9.0.0", + "System.CommandLine": "2.0.0-beta4.24528.1" + }, + "runtime": { + "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll": {} + }, + "resources": { + "cs/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "cs" + }, + "de/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "de" + }, + "es/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "es" + }, + "fr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "fr" + }, + "it/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "it" + }, + "ja/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "ja" + }, + "ko/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "ko" + }, + "pl/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "pl" + }, + "pt-BR/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "pt-BR" + }, + "ru/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "ru" + }, + "tr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "tr" + }, + "zh-Hans/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "zh-Hans" + }, + "zh-Hant/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "Microsoft.Build.Locator/1.6.10": { + "runtime": { + "lib/net6.0/Microsoft.Build.Locator.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.6.10.57384" + } + } + }, + "Microsoft.CodeAnalysis.BannedApiAnalyzers/3.11.0-beta1.24081.1": {}, + "Microsoft.CodeAnalysis.NetAnalyzers/8.0.0-preview.23468.1": {}, + "Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers/3.3.4-beta1.22504.1": {}, + "Microsoft.CodeAnalysis.PublicApiAnalyzers/3.11.0-beta1.24081.1": {}, + "Microsoft.DotNet.XliffTasks/9.0.0-beta.25255.5": {}, + "Microsoft.VisualStudio.Threading.Analyzers/17.13.2": {}, + "Newtonsoft.Json/13.0.3": { + "runtime": { + "lib/net6.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.3.27908" + } + } + }, + "Roslyn.Diagnostics.Analyzers/3.11.0-beta1.24081.1": { + "dependencies": { + "Microsoft.CodeAnalysis.BannedApiAnalyzers": "3.11.0-beta1.24081.1", + "Microsoft.CodeAnalysis.PublicApiAnalyzers": "3.11.0-beta1.24081.1" + } + }, + "System.Collections.Immutable/9.0.0": { + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + }, + "runtime": { + "lib/netstandard2.0/System.Collections.Immutable.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "System.CommandLine/2.0.0-beta4.24528.1": { + "dependencies": { + "System.Memory": "4.5.5" + }, + "runtime": { + "lib/netstandard2.0/System.CommandLine.dll": { + "assemblyVersion": "2.0.0.0", + "fileVersion": "2.0.24.52801" + } + }, + "resources": { + "lib/netstandard2.0/cs/System.CommandLine.resources.dll": { + "locale": "cs" + }, + "lib/netstandard2.0/de/System.CommandLine.resources.dll": { + "locale": "de" + }, + "lib/netstandard2.0/es/System.CommandLine.resources.dll": { + "locale": "es" + }, + "lib/netstandard2.0/fr/System.CommandLine.resources.dll": { + "locale": "fr" + }, + "lib/netstandard2.0/it/System.CommandLine.resources.dll": { + "locale": "it" + }, + "lib/netstandard2.0/ja/System.CommandLine.resources.dll": { + "locale": "ja" + }, + "lib/netstandard2.0/ko/System.CommandLine.resources.dll": { + "locale": "ko" + }, + "lib/netstandard2.0/pl/System.CommandLine.resources.dll": { + "locale": "pl" + }, + "lib/netstandard2.0/pt-BR/System.CommandLine.resources.dll": { + "locale": "pt-BR" + }, + "lib/netstandard2.0/ru/System.CommandLine.resources.dll": { + "locale": "ru" + }, + "lib/netstandard2.0/tr/System.CommandLine.resources.dll": { + "locale": "tr" + }, + "lib/netstandard2.0/zh-Hans/System.CommandLine.resources.dll": { + "locale": "zh-Hans" + }, + "lib/netstandard2.0/zh-Hant/System.CommandLine.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "System.Memory/4.5.5": {}, + "System.Runtime.CompilerServices.Unsafe/6.0.0": {} + } + }, + "libraries": { + "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/4.14.0-3.25262.10": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.Build.Locator/1.6.10": { + "type": "package", + "serviceable": true, + "sha512": "sha512-DJhCkTGqy1LMJzEmG/2qxRTMHwdPc3WdVoGQI5o5mKHVo4dsHrCMLIyruwU/NSvPNSdvONlaf7jdFXnAMuxAuA==", + "path": "microsoft.build.locator/1.6.10", + "hashPath": "microsoft.build.locator.1.6.10.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.BannedApiAnalyzers/3.11.0-beta1.24081.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-DH6L3rsbjppLrHM2l2/NKbnMaYd0NFHx2pjZaFdrVcRkONrV3i9FHv6Id8Dp6/TmjhXQsJVJJFbhhjkpuP1xxg==", + "path": "microsoft.codeanalysis.bannedapianalyzers/3.11.0-beta1.24081.1", + "hashPath": "microsoft.codeanalysis.bannedapianalyzers.3.11.0-beta1.24081.1.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.NetAnalyzers/8.0.0-preview.23468.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ZhIvyxmUCqb8OiU/VQfxfuAmIB4lQsjqhMVYKeoyxzSI+d7uR5Pzx3ZKoaIhPizQ15wa4lnyD6wg3TnSJ6P4LA==", + "path": "microsoft.codeanalysis.netanalyzers/8.0.0-preview.23468.1", + "hashPath": "microsoft.codeanalysis.netanalyzers.8.0.0-preview.23468.1.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers/3.3.4-beta1.22504.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-2XRlqPAzVke7Sb80+UqaC7o57OwfK+tIr+aIOxrx41RWDMeR2SBUW7kL4sd6hfLFfBNsLo3W5PT+UwfvwPaOzA==", + "path": "microsoft.codeanalysis.performancesensitiveanalyzers/3.3.4-beta1.22504.1", + "hashPath": "microsoft.codeanalysis.performancesensitiveanalyzers.3.3.4-beta1.22504.1.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers/3.11.0-beta1.24081.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-3bYGBihvoNO0rhCOG1U9O50/4Q8suZ+glHqQLIAcKvnodSnSW+dYWYzTNb1UbS8pUS8nAUfxSFMwuMup/G5DtQ==", + "path": "microsoft.codeanalysis.publicapianalyzers/3.11.0-beta1.24081.1", + "hashPath": "microsoft.codeanalysis.publicapianalyzers.3.11.0-beta1.24081.1.nupkg.sha512" + }, + "Microsoft.DotNet.XliffTasks/9.0.0-beta.25255.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-bb0fZB5ViPscdfYeWlmtyXJMzNkgcpkV5RWmXktfV9lwIUZgNZmFotUXrdcTyZzrN7v1tQK/Y6BGnbkP9gEsXg==", + "path": "microsoft.dotnet.xlifftasks/9.0.0-beta.25255.5", + "hashPath": "microsoft.dotnet.xlifftasks.9.0.0-beta.25255.5.nupkg.sha512" + }, + "Microsoft.VisualStudio.Threading.Analyzers/17.13.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Qcd8IlaTXZVq3wolBnzby1P7kWihdWaExtD8riumiKuG1sHa8EgjV/o70TMjTaeUMhomBbhfdC9OPwAHoZfnjQ==", + "path": "microsoft.visualstudio.threading.analyzers/17.13.2", + "hashPath": "microsoft.visualstudio.threading.analyzers.17.13.2.nupkg.sha512" + }, + "Newtonsoft.Json/13.0.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==", + "path": "newtonsoft.json/13.0.3", + "hashPath": "newtonsoft.json.13.0.3.nupkg.sha512" + }, + "Roslyn.Diagnostics.Analyzers/3.11.0-beta1.24081.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-reHqZCDKifA+DURcL8jUfYkMGL4FpgNt5LI0uWTS6IpM8kKVbu/kO8byZsqfhBu4wUzT3MBDcoMfzhZPdENIpg==", + "path": "roslyn.diagnostics.analyzers/3.11.0-beta1.24081.1", + "hashPath": "roslyn.diagnostics.analyzers.3.11.0-beta1.24081.1.nupkg.sha512" + }, + "System.Collections.Immutable/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==", + "path": "system.collections.immutable/9.0.0", + "hashPath": "system.collections.immutable.9.0.0.nupkg.sha512" + }, + "System.CommandLine/2.0.0-beta4.24528.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Xt8tsSU8yd0ZpbT9gl5DAwkMYWLo8PV1fq2R/belrUbHVVOIKqhLfbWksbdknUDpmzMHZenBtD6AGAp9uJTa2w==", + "path": "system.commandline/2.0.0-beta4.24528.1", + "hashPath": "system.commandline.2.0.0-beta4.24528.1.nupkg.sha512" + }, + "System.Memory/4.5.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "path": "system.memory/4.5.5", + "hashPath": "system.memory.4.5.5.nupkg.sha512" + }, + "System.Runtime.CompilerServices.Unsafe/6.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", + "path": "system.runtime.compilerservices.unsafe/6.0.0", + "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512" + } + } +} \ No newline at end of file diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll" new file mode 100755 index 00000000..993b54f2 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll.config" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll.config" new file mode 100755 index 00000000..27bdea78 --- /dev/null +++ "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll.config" @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.runtimeconfig.json" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.runtimeconfig.json" new file mode 100755 index 00000000..3a5998aa --- /dev/null +++ "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.runtimeconfig.json" @@ -0,0 +1,13 @@ +{ + "runtimeOptions": { + "tfm": "net6.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "6.0.0" + }, + "rollForward": "Major", + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false + } + } +} \ No newline at end of file diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Newtonsoft.Json.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Newtonsoft.Json.dll" new file mode 100755 index 00000000..87bf9aab Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/Newtonsoft.Json.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/System.Collections.Immutable.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/System.Collections.Immutable.dll" new file mode 100755 index 00000000..b1821271 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/System.Collections.Immutable.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/System.CommandLine.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/System.CommandLine.dll" new file mode 100755 index 00000000..d0bbad5d Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/System.CommandLine.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/cs/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/cs/System.CommandLine.resources.dll" new file mode 100755 index 00000000..0be3757c Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/cs/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/de/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/de/System.CommandLine.resources.dll" new file mode 100755 index 00000000..bfed293e Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/de/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/es/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/es/System.CommandLine.resources.dll" new file mode 100755 index 00000000..5e1c416f Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/es/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/fr/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/fr/System.CommandLine.resources.dll" new file mode 100755 index 00000000..2916bdf2 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/fr/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/it/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/it/System.CommandLine.resources.dll" new file mode 100755 index 00000000..1a55c94f Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/it/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/ja/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/ja/System.CommandLine.resources.dll" new file mode 100755 index 00000000..c1be1539 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/ja/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/ko/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/ko/System.CommandLine.resources.dll" new file mode 100755 index 00000000..bfcbbc61 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/ko/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/pl/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/pl/System.CommandLine.resources.dll" new file mode 100755 index 00000000..b9efaec6 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/pl/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/pt-BR/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/pt-BR/System.CommandLine.resources.dll" new file mode 100755 index 00000000..69612cbc Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/pt-BR/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/ru/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/ru/System.CommandLine.resources.dll" new file mode 100755 index 00000000..042aaf8e Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/ru/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/tr/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/tr/System.CommandLine.resources.dll" new file mode 100755 index 00000000..629b98b4 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/tr/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/zh-Hans/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/zh-Hans/System.CommandLine.resources.dll" new file mode 100755 index 00000000..ff8dacbf Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/zh-Hans/System.CommandLine.resources.dll" differ diff --git "a/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/zh-Hant/System.CommandLine.resources.dll" "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/zh-Hant/System.CommandLine.resources.dll" new file mode 100755 index 00000000..9b9870a0 Binary files /dev/null and "b/src/Melodee.Common/bin\\Debug/net10.0/BuildHost-netcore/zh-Hant/System.CommandLine.resources.dll" differ diff --git a/tests/Melodee.Tests.Common/Services/Parsing/M3UParserTests.cs b/tests/Melodee.Tests.Common/Services/Parsing/M3UParserTests.cs new file mode 100644 index 00000000..f4968f92 --- /dev/null +++ b/tests/Melodee.Tests.Common/Services/Parsing/M3UParserTests.cs @@ -0,0 +1,222 @@ +using System.Text; +using Melodee.Common.Services.Parsing; +using Serilog.Core; + +namespace Melodee.Tests.Common.Services.Parsing; + +public class M3UParserTests +{ + private static M3UParser CreateParser() + { + return new M3UParser(Logger.None); + } + + [Fact] + public async Task ParseAsync_WithSimpleM3U_ParsesEntries() + { + var parser = CreateParser(); + var content = """ + #EXTM3U + Artist/Album/01 - Song One.flac + Artist/Album/02 - Song Two.flac + """; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Equal(2, result.Entries.Count); + Assert.Equal("Artist/Album/01 - Song One.flac", result.Entries[0].NormalizedReference); + Assert.Equal("Artist/Album/02 - Song Two.flac", result.Entries[1].NormalizedReference); + } + + [Fact] + public async Task ParseAsync_SkipsBlankLines_Successfully() + { + var parser = CreateParser(); + var content = """ + #EXTM3U + + Artist/Album/Song.flac + + + Artist/Album/Song2.flac + """; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Equal(2, result.Entries.Count); + } + + [Fact] + public async Task ParseAsync_SkipsComments_Successfully() + { + var parser = CreateParser(); + var content = """ + #EXTM3U + #EXTINF:123,Artist - Title + Artist/Album/Song.flac + # This is a comment + Artist/Album/Song2.flac + """; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Equal(2, result.Entries.Count); + } + + [Fact] + public async Task ParseAsync_NormalizesBackslashes_ToForwardSlashes() + { + var parser = CreateParser(); + var content = @"D:\Music\Artist\Album\Song.mp3"; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Single(result.Entries); + Assert.Equal("D:/Music/Artist/Album/Song.mp3", result.Entries[0].NormalizedReference); + } + + [Fact] + public async Task ParseAsync_DecodesUrlEncodedPaths_Successfully() + { + var parser = CreateParser(); + var content = "Artist/Album/Song%20With%20Spaces.flac"; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Single(result.Entries); + Assert.Equal("Artist/Album/Song With Spaces.flac", result.Entries[0].NormalizedReference); + } + + [Fact] + public async Task ParseAsync_RemovesQuotes_FromPaths() + { + var parser = CreateParser(); + var content = """ + "Artist/Album/Song.flac" + 'Artist/Album/Song2.flac' + """; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Equal(2, result.Entries.Count); + Assert.Equal("Artist/Album/Song.flac", result.Entries[0].NormalizedReference); + Assert.Equal("Artist/Album/Song2.flac", result.Entries[1].NormalizedReference); + } + + [Fact] + public async Task ParseAsync_ExtractsPathHints_Correctly() + { + var parser = CreateParser(); + var content = "Pink Floyd/The Wall/02 - Another Brick in the Wall.flac"; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Single(result.Entries); + var entry = result.Entries[0]; + Assert.Equal("02 - Another Brick in the Wall.flac", entry.FileName); + Assert.Equal("The Wall", entry.AlbumFolder); + Assert.Equal("Pink Floyd", entry.ArtistFolder); + } + + [Fact] + public async Task ParseAsync_HandlesAbsolutePaths_WithMultipleSegments() + { + var parser = CreateParser(); + var content = "/mnt/music/Artist/Album/Song.flac"; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Single(result.Entries); + var entry = result.Entries[0]; + Assert.Equal("Song.flac", entry.FileName); + Assert.Equal("Album", entry.AlbumFolder); + Assert.Equal("Artist", entry.ArtistFolder); + } + + [Fact] + public async Task ParseAsync_HandlesSingleFileNameOnly_WithoutFolders() + { + var parser = CreateParser(); + var content = "song.flac"; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Single(result.Entries); + var entry = result.Entries[0]; + Assert.Equal("song.flac", entry.FileName); + Assert.Null(entry.AlbumFolder); + Assert.Null(entry.ArtistFolder); + } + + [Fact] + public async Task ParseAsync_HandlesM3U8_UsesUTF8Encoding() + { + var parser = CreateParser(); + var content = "Artist/Album/日本語のタイトル.flac"; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u8"); + + Assert.Single(result.Entries); + Assert.Contains("日本語", result.Entries[0].NormalizedReference); + Assert.Equal("utf-8", result.Encoding); + } + + [Fact] + public async Task ParseAsync_PreservesSortOrder_FromLineNumbers() + { + var parser = CreateParser(); + var content = """ + #EXTM3U + First.flac + Second.flac + Third.flac + """; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Equal(3, result.Entries.Count); + Assert.Equal(2, result.Entries[0].SortOrder); + Assert.Equal(3, result.Entries[1].SortOrder); + Assert.Equal(4, result.Entries[2].SortOrder); + } + + [Fact] + public async Task ParseAsync_HandlesUrlsAsEntries_WithoutCrashing() + { + var parser = CreateParser(); + var content = "http://example.com/stream.mp3"; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Single(result.Entries); + Assert.Equal("http://example.com/stream.mp3", result.Entries[0].NormalizedReference); + Assert.Null(result.Entries[0].ArtistFolder); + Assert.Null(result.Entries[0].AlbumFolder); + } + + [Fact] + public async Task ParseAsync_HandlesSpecialCharacters_InPaths() + { + var parser = CreateParser(); + var content = "Artist/Album [2024]/Song (Remastered).flac"; + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var result = await parser.ParseAsync(stream, "test.m3u"); + + Assert.Single(result.Entries); + Assert.Equal("Artist/Album [2024]/Song (Remastered).flac", result.Entries[0].NormalizedReference); + Assert.Equal("Song (Remastered).flac", result.Entries[0].FileName); + } +}